rubinius-net-ldap 0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +462 -0
- data/.travis.yml +19 -0
- data/CONTRIBUTING.md +54 -0
- data/Contributors.rdoc +24 -0
- data/Gemfile +2 -0
- data/Hacking.rdoc +63 -0
- data/History.rdoc +260 -0
- data/License.rdoc +29 -0
- data/README.rdoc +65 -0
- data/Rakefile +17 -0
- data/lib/net-ldap.rb +2 -0
- data/lib/net/ber.rb +320 -0
- data/lib/net/ber/ber_parser.rb +182 -0
- data/lib/net/ber/core_ext.rb +55 -0
- data/lib/net/ber/core_ext/array.rb +96 -0
- data/lib/net/ber/core_ext/false_class.rb +10 -0
- data/lib/net/ber/core_ext/integer.rb +74 -0
- data/lib/net/ber/core_ext/string.rb +66 -0
- data/lib/net/ber/core_ext/true_class.rb +11 -0
- data/lib/net/ldap.rb +1229 -0
- data/lib/net/ldap/connection.rb +702 -0
- data/lib/net/ldap/dataset.rb +168 -0
- data/lib/net/ldap/dn.rb +225 -0
- data/lib/net/ldap/entry.rb +193 -0
- data/lib/net/ldap/error.rb +38 -0
- data/lib/net/ldap/filter.rb +778 -0
- data/lib/net/ldap/instrumentation.rb +23 -0
- data/lib/net/ldap/password.rb +38 -0
- data/lib/net/ldap/pdu.rb +297 -0
- data/lib/net/ldap/version.rb +5 -0
- data/lib/net/snmp.rb +264 -0
- data/rubinius-net-ldap.gemspec +37 -0
- data/script/install-openldap +112 -0
- data/script/package +7 -0
- data/script/release +16 -0
- data/test/ber/core_ext/test_array.rb +22 -0
- data/test/ber/core_ext/test_string.rb +25 -0
- data/test/ber/test_ber.rb +99 -0
- data/test/fixtures/cacert.pem +20 -0
- data/test/fixtures/openldap/memberof.ldif +33 -0
- data/test/fixtures/openldap/retcode.ldif +76 -0
- data/test/fixtures/openldap/slapd.conf.ldif +67 -0
- data/test/fixtures/seed.ldif +374 -0
- data/test/integration/test_add.rb +28 -0
- data/test/integration/test_ber.rb +30 -0
- data/test/integration/test_bind.rb +34 -0
- data/test/integration/test_delete.rb +31 -0
- data/test/integration/test_open.rb +88 -0
- data/test/integration/test_return_codes.rb +38 -0
- data/test/integration/test_search.rb +77 -0
- data/test/support/vm/openldap/.gitignore +1 -0
- data/test/support/vm/openldap/README.md +32 -0
- data/test/support/vm/openldap/Vagrantfile +33 -0
- data/test/test_dn.rb +44 -0
- data/test/test_entry.rb +65 -0
- data/test/test_filter.rb +223 -0
- data/test/test_filter_parser.rb +20 -0
- data/test/test_helper.rb +66 -0
- data/test/test_ldap.rb +60 -0
- data/test/test_ldap_connection.rb +404 -0
- data/test/test_ldif.rb +104 -0
- data/test/test_password.rb +10 -0
- data/test/test_rename.rb +77 -0
- data/test/test_search.rb +39 -0
- data/test/test_snmp.rb +119 -0
- data/test/test_ssl_ber.rb +40 -0
- data/test/testdata.ldif +101 -0
- data/testserver/ldapserver.rb +210 -0
- data/testserver/testdata.ldif +101 -0
- metadata +204 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
class Net::LDAP
|
2
|
+
class LdapError < StandardError
|
3
|
+
def message
|
4
|
+
"Deprecation warning: Net::LDAP::LdapError is no longer used. Use Net::LDAP::Error or rescue one of it's subclasses. \n" + super
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
class AlreadyOpenedError < Error; end
|
11
|
+
class SocketError < Error; end
|
12
|
+
class ConnectionRefusedError < Error; end
|
13
|
+
class NoOpenSSLError < Error; end
|
14
|
+
class NoStartTLSResultError < Error; end
|
15
|
+
class NoSearchBaseError < Error; end
|
16
|
+
class StartTLSError < Error; end
|
17
|
+
class EncryptionUnsupportedError < Error; end
|
18
|
+
class EncMethodUnsupportedError < Error; end
|
19
|
+
class AuthMethodUnsupportedError < Error; end
|
20
|
+
class BindingInformationInvalidError < Error; end
|
21
|
+
class NoBindResultError < Error; end
|
22
|
+
class SASLChallengeOverflowError < Error; end
|
23
|
+
class SearchSizeInvalidError < Error; end
|
24
|
+
class SearchScopeInvalidError < Error; end
|
25
|
+
class ResponseTypeInvalidError < Error; end
|
26
|
+
class ResponseMissingOrInvalidError < Error; end
|
27
|
+
class EmptyDNError < Error; end
|
28
|
+
class HashTypeUnsupportedError < Error; end
|
29
|
+
class OperatorError < Error; end
|
30
|
+
class SubstringFilterError < Error; end
|
31
|
+
class SearchFilterError < Error; end
|
32
|
+
class BERInvalidError < Error; end
|
33
|
+
class SearchFilterTypeUnknownError < Error; end
|
34
|
+
class BadAttributeError < Error; end
|
35
|
+
class FilterTypeUnknownError < Error; end
|
36
|
+
class FilterSyntaxInvalidError < Error; end
|
37
|
+
class EntryOverflowError < Error; end
|
38
|
+
end
|
@@ -0,0 +1,778 @@
|
|
1
|
+
# -*- ruby encoding: utf-8 -*-
|
2
|
+
|
3
|
+
##
|
4
|
+
# Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
|
5
|
+
# this class is passed to Net::LDAP#search in the parameter :filter.
|
6
|
+
#
|
7
|
+
# Net::LDAP::Filter supports the complete set of search filters available in
|
8
|
+
# LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
|
9
|
+
# This class supplants the (infamous) RFC 2254 standard notation for
|
10
|
+
# specifying LDAP search filters.
|
11
|
+
#--
|
12
|
+
# NOTE: This wording needs to change as we will be supporting LDAPv3 search
|
13
|
+
# filter strings (RFC 4515).
|
14
|
+
#++
|
15
|
+
#
|
16
|
+
# Here's how to code the familiar "objectclass is present" filter:
|
17
|
+
# f = Net::LDAP::Filter.present("objectclass")
|
18
|
+
#
|
19
|
+
# The object returned by this code can be passed directly to the
|
20
|
+
# <tt>:filter</tt> parameter of Net::LDAP#search.
|
21
|
+
#
|
22
|
+
# See the individual class and instance methods below for more examples.
|
23
|
+
class Net::LDAP::Filter
|
24
|
+
##
|
25
|
+
# Known filter types.
|
26
|
+
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex, :bineq ]
|
27
|
+
|
28
|
+
def initialize(op, left, right) #:nodoc:
|
29
|
+
unless FilterTypes.include?(op)
|
30
|
+
raise Net::LDAP::OperatorError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
|
31
|
+
end
|
32
|
+
@op = op
|
33
|
+
@left = left
|
34
|
+
@right = right
|
35
|
+
end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
# We don't want filters created except using our custom constructors.
|
39
|
+
private :new
|
40
|
+
|
41
|
+
##
|
42
|
+
# Creates a Filter object indicating that the value of a particular
|
43
|
+
# attribute must either be present or match a particular string.
|
44
|
+
#
|
45
|
+
# Specifying that an attribute is 'present' means only directory entries
|
46
|
+
# which contain a value for the particular attribute will be selected by
|
47
|
+
# the filter. This is useful in case of optional attributes such as
|
48
|
+
# <tt>mail.</tt> Presence is indicated by giving the value "*" in the
|
49
|
+
# second parameter to #eq. This example selects only entries that have
|
50
|
+
# one or more values for <tt>sAMAccountName:</tt>
|
51
|
+
#
|
52
|
+
# f = Net::LDAP::Filter.eq("sAMAccountName", "*")
|
53
|
+
#
|
54
|
+
# To match a particular range of values, pass a string as the second
|
55
|
+
# parameter to #eq. The string may contain one or more "*" characters as
|
56
|
+
# wildcards: these match zero or more occurrences of any character. Full
|
57
|
+
# regular-expressions are <i>not</i> supported due to limitations in the
|
58
|
+
# underlying LDAP protocol. This example selects any entry with a
|
59
|
+
# <tt>mail</tt> value containing the substring "anderson":
|
60
|
+
#
|
61
|
+
# f = Net::LDAP::Filter.eq("mail", "*anderson*")
|
62
|
+
#
|
63
|
+
# This filter does not perform any escaping
|
64
|
+
def eq(attribute, value)
|
65
|
+
new(:eq, attribute, value)
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Creates a Filter object indicating a binary comparison.
|
70
|
+
# this prevents the search data from being forced into a UTF-8 string.
|
71
|
+
#
|
72
|
+
# This is primarily used for Microsoft Active Directory to compare
|
73
|
+
# GUID values.
|
74
|
+
#
|
75
|
+
# # for guid represented as hex charecters
|
76
|
+
# guid = "6a31b4a12aa27a41aca9603f27dd5116"
|
77
|
+
# guid_bin = [guid].pack("H*")
|
78
|
+
# f = Net::LDAP::Filter.bineq("objectGUID", guid_bin)
|
79
|
+
#
|
80
|
+
# This filter does not perform any escaping.
|
81
|
+
def bineq(attribute, value)
|
82
|
+
new(:bineq, attribute, value)
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Creates a Filter object indicating extensible comparison. This Filter
|
87
|
+
# object is currently considered EXPERIMENTAL.
|
88
|
+
#
|
89
|
+
# sample_attributes = ['cn:fr', 'cn:fr.eq',
|
90
|
+
# 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
|
91
|
+
# attr = sample_attributes.first # Pick an extensible attribute
|
92
|
+
# value = 'roberts'
|
93
|
+
#
|
94
|
+
# filter = "#{attr}:=#{value}" # Basic String Filter
|
95
|
+
# filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
|
96
|
+
#
|
97
|
+
# # Perform a search with the Extensible Match Filter
|
98
|
+
# Net::LDAP.search(:filter => filter)
|
99
|
+
#--
|
100
|
+
# The LDIF required to support the above examples on the OpenDS LDAP
|
101
|
+
# server:
|
102
|
+
#
|
103
|
+
# version: 1
|
104
|
+
#
|
105
|
+
# dn: dc=example,dc=com
|
106
|
+
# objectClass: domain
|
107
|
+
# objectClass: top
|
108
|
+
# dc: example
|
109
|
+
#
|
110
|
+
# dn: ou=People,dc=example,dc=com
|
111
|
+
# objectClass: organizationalUnit
|
112
|
+
# objectClass: top
|
113
|
+
# ou: People
|
114
|
+
#
|
115
|
+
# dn: uid=1,ou=People,dc=example,dc=com
|
116
|
+
# objectClass: person
|
117
|
+
# objectClass: organizationalPerson
|
118
|
+
# objectClass: inetOrgPerson
|
119
|
+
# objectClass: top
|
120
|
+
# cn:: csO0YsOpcnRz
|
121
|
+
# sn:: YsO0YiByw7Riw6lydHM=
|
122
|
+
# givenName:: YsO0Yg==
|
123
|
+
# uid: 1
|
124
|
+
#
|
125
|
+
# =Refs:
|
126
|
+
# * http://www.ietf.org/rfc/rfc2251.txt
|
127
|
+
# * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
|
128
|
+
# * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
|
129
|
+
#++
|
130
|
+
def ex(attribute, value)
|
131
|
+
new(:ex, attribute, value)
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Creates a Filter object indicating that a particular attribute value
|
136
|
+
# is either not present or does not match a particular string; see
|
137
|
+
# Filter::eq for more information.
|
138
|
+
#
|
139
|
+
# This filter does not perform any escaping
|
140
|
+
def ne(attribute, value)
|
141
|
+
new(:ne, attribute, value)
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Creates a Filter object indicating that the value of a particular
|
146
|
+
# attribute must match a particular string. The attribute value is
|
147
|
+
# escaped, so the "*" character is interpreted literally.
|
148
|
+
def equals(attribute, value)
|
149
|
+
new(:eq, attribute, escape(value))
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Creates a Filter object indicating that the value of a particular
|
154
|
+
# attribute must begin with a particular string. The attribute value is
|
155
|
+
# escaped, so the "*" character is interpreted literally.
|
156
|
+
def begins(attribute, value)
|
157
|
+
new(:eq, attribute, escape(value) + "*")
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Creates a Filter object indicating that the value of a particular
|
162
|
+
# attribute must end with a particular string. The attribute value is
|
163
|
+
# escaped, so the "*" character is interpreted literally.
|
164
|
+
def ends(attribute, value)
|
165
|
+
new(:eq, attribute, "*" + escape(value))
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# Creates a Filter object indicating that the value of a particular
|
170
|
+
# attribute must contain a particular string. The attribute value is
|
171
|
+
# escaped, so the "*" character is interpreted literally.
|
172
|
+
def contains(attribute, value)
|
173
|
+
new(:eq, attribute, "*" + escape(value) + "*")
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Creates a Filter object indicating that a particular attribute value
|
178
|
+
# is greater than or equal to the specified value.
|
179
|
+
def ge(attribute, value)
|
180
|
+
new(:ge, attribute, value)
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Creates a Filter object indicating that a particular attribute value
|
185
|
+
# is less than or equal to the specified value.
|
186
|
+
def le(attribute, value)
|
187
|
+
new(:le, attribute, value)
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Joins two or more filters so that all conditions must be true. Calling
|
192
|
+
# <tt>Filter.join(left, right)</tt> is the same as <tt>left &
|
193
|
+
# right</tt>.
|
194
|
+
#
|
195
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
196
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
197
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
198
|
+
# # with "George".
|
199
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
200
|
+
# # Selects only entries that meet both conditions above.
|
201
|
+
# z = Net::LDAP::Filter.join(x, y)
|
202
|
+
def join(left, right)
|
203
|
+
new(:and, left, right)
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Creates a disjoint comparison between two or more filters. Selects
|
208
|
+
# entries where either the left or right side are true. Calling
|
209
|
+
# <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
|
210
|
+
# right</tt>.
|
211
|
+
#
|
212
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
213
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
214
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
215
|
+
# # with "George".
|
216
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
217
|
+
# # Selects only entries that meet either condition above.
|
218
|
+
# z = x | y
|
219
|
+
def intersect(left, right)
|
220
|
+
new(:or, left, right)
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
|
225
|
+
# as <tt>~filter</tt>.
|
226
|
+
#
|
227
|
+
# # Selects only entries that do not have an <tt>objectclass</tt>
|
228
|
+
# # attribute.
|
229
|
+
# x = ~Net::LDAP::Filter.present("objectclass")
|
230
|
+
def negate(filter)
|
231
|
+
new(:not, filter, nil)
|
232
|
+
end
|
233
|
+
|
234
|
+
##
|
235
|
+
# This is a synonym for #eq(attribute, "*"). Also known as #present and
|
236
|
+
# #pres.
|
237
|
+
def present?(attribute)
|
238
|
+
eq(attribute, "*")
|
239
|
+
end
|
240
|
+
alias_method :present, :present?
|
241
|
+
alias_method :pres, :present?
|
242
|
+
|
243
|
+
# http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1
|
244
|
+
# charset for filters. All of the following must be escaped in any normal
|
245
|
+
# string using a single backslash ('\') as escape.
|
246
|
+
#
|
247
|
+
ESCAPES = {
|
248
|
+
"\0" => '00', # NUL = %x00 ; null character
|
249
|
+
'*' => '2A', # ASTERISK = %x2A ; asterisk ("*")
|
250
|
+
'(' => '28', # LPARENS = %x28 ; left parenthesis ("(")
|
251
|
+
')' => '29', # RPARENS = %x29 ; right parenthesis (")")
|
252
|
+
'\\' => '5C', # ESC = %x5C ; esc (or backslash) ("\")
|
253
|
+
}
|
254
|
+
# Compiled character class regexp using the keys from the above hash.
|
255
|
+
ESCAPE_RE = Regexp.new(
|
256
|
+
"[" +
|
257
|
+
ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
|
258
|
+
"]")
|
259
|
+
|
260
|
+
##
|
261
|
+
# Escape a string for use in an LDAP filter
|
262
|
+
def escape(string)
|
263
|
+
string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
|
264
|
+
end
|
265
|
+
|
266
|
+
##
|
267
|
+
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter
|
268
|
+
# object. The incoming BER object most likely came to us by parsing an
|
269
|
+
# LDAP searchRequest PDU. See also the comments under #to_ber, including
|
270
|
+
# the grammar snippet from the RFC.
|
271
|
+
#--
|
272
|
+
# We're hardcoding the BER constants from the RFC. These should be
|
273
|
+
# broken out insto constants.
|
274
|
+
def parse_ber(ber)
|
275
|
+
case ber.ber_identifier
|
276
|
+
when 0xa0 # context-specific constructed 0, "and"
|
277
|
+
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
|
278
|
+
when 0xa1 # context-specific constructed 1, "or"
|
279
|
+
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
|
280
|
+
when 0xa2 # context-specific constructed 2, "not"
|
281
|
+
~parse_ber(ber.first)
|
282
|
+
when 0xa3 # context-specific constructed 3, "equalityMatch"
|
283
|
+
if ber.last == "*"
|
284
|
+
else
|
285
|
+
eq(ber.first, ber.last)
|
286
|
+
end
|
287
|
+
when 0xa4 # context-specific constructed 4, "substring"
|
288
|
+
str = ""
|
289
|
+
final = false
|
290
|
+
ber.last.each { |b|
|
291
|
+
case b.ber_identifier
|
292
|
+
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
|
293
|
+
raise Net::LDAP::SubstringFilterError, "Unrecognized substring filter; bad initial value." if str.length > 0
|
294
|
+
str += escape(b)
|
295
|
+
when 0x81 # context-specific primitive 0, SubstringFilter "any"
|
296
|
+
str += "*#{escape(b)}"
|
297
|
+
when 0x82 # context-specific primitive 0, SubstringFilter "final"
|
298
|
+
str += "*#{escape(b)}"
|
299
|
+
final = true
|
300
|
+
end
|
301
|
+
}
|
302
|
+
str += "*" unless final
|
303
|
+
eq(ber.first.to_s, str)
|
304
|
+
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
|
305
|
+
ge(ber.first.to_s, ber.last.to_s)
|
306
|
+
when 0xa6 # context-specific constructed 6, "lessOrEqual"
|
307
|
+
le(ber.first.to_s, ber.last.to_s)
|
308
|
+
when 0x87 # context-specific primitive 7, "present"
|
309
|
+
# call to_s to get rid of the BER-identifiedness of the incoming string.
|
310
|
+
present?(ber.to_s)
|
311
|
+
when 0xa9 # context-specific constructed 9, "extensible comparison"
|
312
|
+
raise Net::LDAP::SearchFilterError, "Invalid extensible search filter, should be at least two elements" if ber.size < 2
|
313
|
+
|
314
|
+
# Reassembles the extensible filter parts
|
315
|
+
# (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
|
316
|
+
type = value = dn = rule = nil
|
317
|
+
ber.each do |element|
|
318
|
+
case element.ber_identifier
|
319
|
+
when 0x81 then rule=element
|
320
|
+
when 0x82 then type=element
|
321
|
+
when 0x83 then value=element
|
322
|
+
when 0x84 then dn='dn'
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
attribute = ''
|
327
|
+
attribute << type if type
|
328
|
+
attribute << ":#{dn}" if dn
|
329
|
+
attribute << ":#{rule}" if rule
|
330
|
+
|
331
|
+
ex(attribute, value)
|
332
|
+
else
|
333
|
+
raise Net::LDAP::BERInvalidError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
|
339
|
+
# to a Net::LDAP::Filter.
|
340
|
+
def construct(ldap_filter_string)
|
341
|
+
FilterParser.parse(ldap_filter_string)
|
342
|
+
end
|
343
|
+
alias_method :from_rfc2254, :construct
|
344
|
+
alias_method :from_rfc4515, :construct
|
345
|
+
|
346
|
+
##
|
347
|
+
# Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
|
348
|
+
# object.
|
349
|
+
#--
|
350
|
+
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
|
351
|
+
# filter types. Could pull them out into a constant.
|
352
|
+
#++
|
353
|
+
def parse_ldap_filter(obj)
|
354
|
+
case obj.ber_identifier
|
355
|
+
when 0x87 # present. context-specific primitive 7.
|
356
|
+
eq(obj.to_s, "*")
|
357
|
+
when 0xa3 # equalityMatch. context-specific constructed 3.
|
358
|
+
eq(obj[0], obj[1])
|
359
|
+
else
|
360
|
+
raise Net::LDAP::SearchFilterTypeUnknownError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
##
|
366
|
+
# Joins two or more filters so that all conditions must be true.
|
367
|
+
#
|
368
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
369
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
370
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
371
|
+
# # with "George".
|
372
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
373
|
+
# # Selects only entries that meet both conditions above.
|
374
|
+
# z = x & y
|
375
|
+
def &(filter)
|
376
|
+
self.class.join(self, filter)
|
377
|
+
end
|
378
|
+
|
379
|
+
##
|
380
|
+
# Creates a disjoint comparison between two or more filters. Selects
|
381
|
+
# entries where either the left or right side are true.
|
382
|
+
#
|
383
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
384
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
385
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
386
|
+
# # with "George".
|
387
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
388
|
+
# # Selects only entries that meet either condition above.
|
389
|
+
# z = x | y
|
390
|
+
def |(filter)
|
391
|
+
self.class.intersect(self, filter)
|
392
|
+
end
|
393
|
+
|
394
|
+
##
|
395
|
+
# Negates a filter.
|
396
|
+
#
|
397
|
+
# # Selects only entries that do not have an <tt>objectclass</tt>
|
398
|
+
# # attribute.
|
399
|
+
# x = ~Net::LDAP::Filter.present("objectclass")
|
400
|
+
def ~@
|
401
|
+
self.class.negate(self)
|
402
|
+
end
|
403
|
+
|
404
|
+
##
|
405
|
+
# Equality operator for filters, useful primarily for constructing unit tests.
|
406
|
+
def ==(filter)
|
407
|
+
# 20100320 AZ: We need to come up with a better way of doing this. This
|
408
|
+
# is just nasty.
|
409
|
+
str = "[@op,@left,@right]"
|
410
|
+
self.instance_eval(str) == filter.instance_eval(str)
|
411
|
+
end
|
412
|
+
|
413
|
+
def to_raw_rfc2254
|
414
|
+
case @op
|
415
|
+
when :ne
|
416
|
+
"!(#{@left}=#{@right})"
|
417
|
+
when :eq, :bineq
|
418
|
+
"#{@left}=#{@right}"
|
419
|
+
when :ex
|
420
|
+
"#{@left}:=#{@right}"
|
421
|
+
when :ge
|
422
|
+
"#{@left}>=#{@right}"
|
423
|
+
when :le
|
424
|
+
"#{@left}<=#{@right}"
|
425
|
+
when :and
|
426
|
+
"&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
427
|
+
when :or
|
428
|
+
"|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
429
|
+
when :not
|
430
|
+
"!(#{@left.to_raw_rfc2254})"
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
##
|
435
|
+
# Converts the Filter object to an RFC 2254-compatible text format.
|
436
|
+
def to_rfc2254
|
437
|
+
"(#{to_raw_rfc2254})"
|
438
|
+
end
|
439
|
+
|
440
|
+
def to_s
|
441
|
+
to_rfc2254
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
# Converts the filter to BER format.
|
446
|
+
#--
|
447
|
+
# Filter ::=
|
448
|
+
# CHOICE {
|
449
|
+
# and [0] SET OF Filter,
|
450
|
+
# or [1] SET OF Filter,
|
451
|
+
# not [2] Filter,
|
452
|
+
# equalityMatch [3] AttributeValueAssertion,
|
453
|
+
# substrings [4] SubstringFilter,
|
454
|
+
# greaterOrEqual [5] AttributeValueAssertion,
|
455
|
+
# lessOrEqual [6] AttributeValueAssertion,
|
456
|
+
# present [7] AttributeType,
|
457
|
+
# approxMatch [8] AttributeValueAssertion,
|
458
|
+
# extensibleMatch [9] MatchingRuleAssertion
|
459
|
+
# }
|
460
|
+
#
|
461
|
+
# SubstringFilter ::=
|
462
|
+
# SEQUENCE {
|
463
|
+
# type AttributeType,
|
464
|
+
# SEQUENCE OF CHOICE {
|
465
|
+
# initial [0] LDAPString,
|
466
|
+
# any [1] LDAPString,
|
467
|
+
# final [2] LDAPString
|
468
|
+
# }
|
469
|
+
# }
|
470
|
+
#
|
471
|
+
# MatchingRuleAssertion ::=
|
472
|
+
# SEQUENCE {
|
473
|
+
# matchingRule [1] MatchingRuleId OPTIONAL,
|
474
|
+
# type [2] AttributeDescription OPTIONAL,
|
475
|
+
# matchValue [3] AssertionValue,
|
476
|
+
# dnAttributes [4] BOOLEAN DEFAULT FALSE
|
477
|
+
# }
|
478
|
+
#
|
479
|
+
# Matching Rule Suffixes
|
480
|
+
# Less than [.1] or .[lt]
|
481
|
+
# Less than or equal to [.2] or [.lte]
|
482
|
+
# Equality [.3] or [.eq] (default)
|
483
|
+
# Greater than or equal to [.4] or [.gte]
|
484
|
+
# Greater than [.5] or [.gt]
|
485
|
+
# Substring [.6] or [.sub]
|
486
|
+
#
|
487
|
+
#++
|
488
|
+
def to_ber
|
489
|
+
case @op
|
490
|
+
when :eq
|
491
|
+
if @right == "*" # presence test
|
492
|
+
@left.to_s.to_ber_contextspecific(7)
|
493
|
+
elsif @right =~ /[*]/ # substring
|
494
|
+
# Parsing substrings is a little tricky. We use String#split to
|
495
|
+
# break a string into substrings delimited by the * (star)
|
496
|
+
# character. But we also need to know whether there is a star at the
|
497
|
+
# head and tail of the string, so we use a limit parameter value of
|
498
|
+
# -1: "If negative, there is no limit to the number of fields
|
499
|
+
# returned, and trailing null fields are not suppressed."
|
500
|
+
#
|
501
|
+
# 20100320 AZ: This is much simpler than the previous verison. Also,
|
502
|
+
# unnecessary regex escaping has been removed.
|
503
|
+
|
504
|
+
ary = @right.split(/[*]+/, -1)
|
505
|
+
|
506
|
+
if ary.first.empty?
|
507
|
+
first = nil
|
508
|
+
ary.shift
|
509
|
+
else
|
510
|
+
first = unescape(ary.shift).to_ber_contextspecific(0)
|
511
|
+
end
|
512
|
+
|
513
|
+
if ary.last.empty?
|
514
|
+
last = nil
|
515
|
+
ary.pop
|
516
|
+
else
|
517
|
+
last = unescape(ary.pop).to_ber_contextspecific(2)
|
518
|
+
end
|
519
|
+
|
520
|
+
seq = ary.map { |e| unescape(e).to_ber_contextspecific(1) }
|
521
|
+
seq.unshift first if first
|
522
|
+
seq.push last if last
|
523
|
+
|
524
|
+
[@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
|
525
|
+
else # equality
|
526
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
|
527
|
+
end
|
528
|
+
when :bineq
|
529
|
+
# make sure data is not forced to UTF-8
|
530
|
+
[@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3)
|
531
|
+
when :ex
|
532
|
+
seq = []
|
533
|
+
|
534
|
+
unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
|
535
|
+
raise Net::LDAP::BadAttributeError, "Bad attribute #{@left}"
|
536
|
+
end
|
537
|
+
type, dn, rule = $1, $2, $4
|
538
|
+
|
539
|
+
seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
|
540
|
+
seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
|
541
|
+
seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
|
542
|
+
seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
|
543
|
+
|
544
|
+
seq.to_ber_contextspecific(9)
|
545
|
+
when :ge
|
546
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
|
547
|
+
when :le
|
548
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
|
549
|
+
when :ne
|
550
|
+
[self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
|
551
|
+
when :and
|
552
|
+
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
|
553
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
|
554
|
+
when :or
|
555
|
+
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
556
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
|
557
|
+
when :not
|
558
|
+
[@left.to_ber].to_ber_contextspecific(2)
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
##
|
563
|
+
# Perform filter operations against a user-supplied block. This is useful
|
564
|
+
# when implementing an LDAP directory server. The caller's block will be
|
565
|
+
# called with two arguments: first, a symbol denoting the "operation" of
|
566
|
+
# the filter; and second, an array consisting of arguments to the
|
567
|
+
# operation. The user-supplied block (which is MANDATORY) should perform
|
568
|
+
# some desired application-defined processing, and may return a
|
569
|
+
# locally-meaningful object that will appear as a parameter in the :and,
|
570
|
+
# :or and :not operations detailed below.
|
571
|
+
#
|
572
|
+
# A typical object to return from the user-supplied block is an array of
|
573
|
+
# Net::LDAP::Filter objects.
|
574
|
+
#
|
575
|
+
# These are the possible values that may be passed to the user-supplied
|
576
|
+
# block:
|
577
|
+
# * :equalityMatch (the arguments will be an attribute name and a value
|
578
|
+
# to be matched);
|
579
|
+
# * :substrings (two arguments: an attribute name and a value containing
|
580
|
+
# one or more "*" characters);
|
581
|
+
# * :present (one argument: an attribute name);
|
582
|
+
# * :greaterOrEqual (two arguments: an attribute name and a value to be
|
583
|
+
# compared against);
|
584
|
+
# * :lessOrEqual (two arguments: an attribute name and a value to be
|
585
|
+
# compared against);
|
586
|
+
# * :and (two or more arguments, each of which is an object returned
|
587
|
+
# from a recursive call to #execute, with the same block;
|
588
|
+
# * :or (two or more arguments, each of which is an object returned from
|
589
|
+
# a recursive call to #execute, with the same block; and
|
590
|
+
# * :not (one argument, which is an object returned from a recursive
|
591
|
+
# call to #execute with the the same block.
|
592
|
+
def execute(&block)
|
593
|
+
case @op
|
594
|
+
when :eq
|
595
|
+
if @right == "*"
|
596
|
+
yield :present, @left
|
597
|
+
elsif @right.index '*'
|
598
|
+
yield :substrings, @left, @right
|
599
|
+
else
|
600
|
+
yield :equalityMatch, @left, @right
|
601
|
+
end
|
602
|
+
when :ge
|
603
|
+
yield :greaterOrEqual, @left, @right
|
604
|
+
when :le
|
605
|
+
yield :lessOrEqual, @left, @right
|
606
|
+
when :or, :and
|
607
|
+
yield @op, (@left.execute(&block)), (@right.execute(&block))
|
608
|
+
when :not
|
609
|
+
yield @op, (@left.execute(&block))
|
610
|
+
end || []
|
611
|
+
end
|
612
|
+
|
613
|
+
##
|
614
|
+
# This is a private helper method for dealing with chains of ANDs and ORs
|
615
|
+
# that are longer than two. If BOTH of our branches are of the specified
|
616
|
+
# type of joining operator, then return both of them as an array (calling
|
617
|
+
# coalesce recursively). If they're not, then return an array consisting
|
618
|
+
# only of self.
|
619
|
+
def coalesce(operator) #:nodoc:
|
620
|
+
if @op == operator
|
621
|
+
[@left.coalesce(operator), @right.coalesce(operator)]
|
622
|
+
else
|
623
|
+
[self]
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
##
|
628
|
+
#--
|
629
|
+
# We got a hash of attribute values.
|
630
|
+
# Do we match the attributes?
|
631
|
+
# Return T/F, and call match recursively as necessary.
|
632
|
+
#++
|
633
|
+
def match(entry)
|
634
|
+
case @op
|
635
|
+
when :eq
|
636
|
+
if @right == "*"
|
637
|
+
l = entry[@left] and l.length > 0
|
638
|
+
else
|
639
|
+
l = entry[@left] and l = Array(l) and l.index(@right)
|
640
|
+
end
|
641
|
+
else
|
642
|
+
raise Net::LDAP::FilterTypeUnknownError, "Unknown filter type in match: #{@op}"
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
##
|
647
|
+
# Converts escaped characters (e.g., "\\28") to unescaped characters
|
648
|
+
def unescape(right)
|
649
|
+
right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
|
650
|
+
end
|
651
|
+
private :unescape
|
652
|
+
|
653
|
+
##
|
654
|
+
# Parses RFC 2254-style string representations of LDAP filters into Filter
|
655
|
+
# object hierarchies.
|
656
|
+
class FilterParser #:nodoc:
|
657
|
+
##
|
658
|
+
# The constructed filter.
|
659
|
+
attr_reader :filter
|
660
|
+
|
661
|
+
class << self
|
662
|
+
private :new
|
663
|
+
|
664
|
+
##
|
665
|
+
# Construct a filter tree from the provided string and return it.
|
666
|
+
def parse(ldap_filter_string)
|
667
|
+
new(ldap_filter_string).filter
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
def initialize(str)
|
672
|
+
require 'strscan' # Don't load strscan until we need it.
|
673
|
+
@filter = parse(StringScanner.new(str))
|
674
|
+
raise Net::LDAP::FilterSyntaxInvalidError, "Invalid filter syntax." unless @filter
|
675
|
+
end
|
676
|
+
|
677
|
+
##
|
678
|
+
# Parse the string contained in the StringScanner provided. Parsing
|
679
|
+
# tries to parse a standalone expression first. If that fails, it tries
|
680
|
+
# to parse a parenthesized expression.
|
681
|
+
def parse(scanner)
|
682
|
+
parse_filter_branch(scanner) or parse_paren_expression(scanner)
|
683
|
+
end
|
684
|
+
private :parse
|
685
|
+
|
686
|
+
##
|
687
|
+
# Join ("&") and intersect ("|") operations are presented in branches.
|
688
|
+
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
|
689
|
+
# test1 and test2. Each of these is parsed separately and then pushed
|
690
|
+
# into a branch array for filter merging using the parent operation.
|
691
|
+
#
|
692
|
+
# This method parses the branch text out into an array of filter
|
693
|
+
# objects.
|
694
|
+
def parse_branches(scanner)
|
695
|
+
branches = []
|
696
|
+
while branch = parse_paren_expression(scanner)
|
697
|
+
branches << branch
|
698
|
+
end
|
699
|
+
branches
|
700
|
+
end
|
701
|
+
private :parse_branches
|
702
|
+
|
703
|
+
##
|
704
|
+
# Join ("&") and intersect ("|") operations are presented in branches.
|
705
|
+
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
|
706
|
+
# test1 and test2. Each of these is parsed separately and then pushed
|
707
|
+
# into a branch array for filter merging using the parent operation.
|
708
|
+
#
|
709
|
+
# This method calls #parse_branches to generate the branch list and then
|
710
|
+
# merges them into a single Filter tree by calling the provided
|
711
|
+
# operation.
|
712
|
+
def merge_branches(op, scanner)
|
713
|
+
filter = nil
|
714
|
+
branches = parse_branches(scanner)
|
715
|
+
|
716
|
+
if branches.size >= 1
|
717
|
+
filter = branches.shift
|
718
|
+
while not branches.empty?
|
719
|
+
filter = filter.__send__(op, branches.shift)
|
720
|
+
end
|
721
|
+
end
|
722
|
+
|
723
|
+
filter
|
724
|
+
end
|
725
|
+
private :merge_branches
|
726
|
+
|
727
|
+
def parse_paren_expression(scanner)
|
728
|
+
if scanner.scan(/\s*\(\s*/)
|
729
|
+
expr = if scanner.scan(/\s*\&\s*/)
|
730
|
+
merge_branches(:&, scanner)
|
731
|
+
elsif scanner.scan(/\s*\|\s*/)
|
732
|
+
merge_branches(:|, scanner)
|
733
|
+
elsif scanner.scan(/\s*\!\s*/)
|
734
|
+
br = parse_paren_expression(scanner)
|
735
|
+
~br if br
|
736
|
+
else
|
737
|
+
parse_filter_branch(scanner)
|
738
|
+
end
|
739
|
+
|
740
|
+
if expr and scanner.scan(/\s*\)\s*/)
|
741
|
+
expr
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
private :parse_paren_expression
|
746
|
+
|
747
|
+
##
|
748
|
+
# This parses a given expression inside of parentheses.
|
749
|
+
def parse_filter_branch(scanner)
|
750
|
+
scanner.scan(/\s*/)
|
751
|
+
if token = scanner.scan(/[-\w:.]*[\w]/)
|
752
|
+
scanner.scan(/\s*/)
|
753
|
+
if op = scanner.scan(/<=|>=|!=|:=|=/)
|
754
|
+
scanner.scan(/\s*/)
|
755
|
+
if value = scanner.scan(/(?:[-\[\]{}\w*.+:@=,#\$%&!'^~\s\xC3\x80-\xCA\xAF]|[^\x00-\x7F]|\\[a-fA-F\d]{2})+/u)
|
756
|
+
# 20100313 AZ: Assumes that "(uid=george*)" is the same as
|
757
|
+
# "(uid=george* )". The standard doesn't specify, but I can find
|
758
|
+
# no examples that suggest otherwise.
|
759
|
+
value.strip!
|
760
|
+
case op
|
761
|
+
when "="
|
762
|
+
Net::LDAP::Filter.eq(token, value)
|
763
|
+
when "!="
|
764
|
+
Net::LDAP::Filter.ne(token, value)
|
765
|
+
when "<="
|
766
|
+
Net::LDAP::Filter.le(token, value)
|
767
|
+
when ">="
|
768
|
+
Net::LDAP::Filter.ge(token, value)
|
769
|
+
when ":="
|
770
|
+
Net::LDAP::Filter.ex(token, value)
|
771
|
+
end
|
772
|
+
end
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
776
|
+
private :parse_filter_branch
|
777
|
+
end # class Net::LDAP::FilterParser
|
778
|
+
end # class Net::LDAP::Filter
|