treequel 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +354 -0
- data/LICENSE +27 -0
- data/README +66 -0
- data/Rakefile +345 -0
- data/Rakefile.local +43 -0
- data/bin/treeirb +14 -0
- data/bin/treequel +229 -0
- data/examples/company-directory.rb +112 -0
- data/examples/ldap-monitor.rb +143 -0
- data/examples/ldap-monitor/public/css/master.css +328 -0
- data/examples/ldap-monitor/public/images/card_small.png +0 -0
- data/examples/ldap-monitor/public/images/chain_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
- data/examples/ldap-monitor/public/images/plug.png +0 -0
- data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
- data/examples/ldap-monitor/public/images/tick.png +0 -0
- data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
- data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
- data/examples/ldap-monitor/views/backends.erb +41 -0
- data/examples/ldap-monitor/views/connections.erb +74 -0
- data/examples/ldap-monitor/views/databases.erb +39 -0
- data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
- data/examples/ldap-monitor/views/index.erb +14 -0
- data/examples/ldap-monitor/views/layout.erb +35 -0
- data/examples/ldap-monitor/views/listeners.erb +30 -0
- data/examples/ldap_state.rb +62 -0
- data/lib/treequel.rb +145 -0
- data/lib/treequel/branch.rb +589 -0
- data/lib/treequel/branchcollection.rb +204 -0
- data/lib/treequel/branchset.rb +360 -0
- data/lib/treequel/constants.rb +604 -0
- data/lib/treequel/directory.rb +541 -0
- data/lib/treequel/exceptions.rb +32 -0
- data/lib/treequel/filter.rb +704 -0
- data/lib/treequel/mixins.rb +325 -0
- data/lib/treequel/schema.rb +245 -0
- data/lib/treequel/schema/attributetype.rb +252 -0
- data/lib/treequel/schema/ldapsyntax.rb +96 -0
- data/lib/treequel/schema/matchingrule.rb +124 -0
- data/lib/treequel/schema/matchingruleuse.rb +124 -0
- data/lib/treequel/schema/objectclass.rb +289 -0
- data/lib/treequel/sequel_integration.rb +26 -0
- data/lib/treequel/utils.rb +169 -0
- data/rake/191_compat.rb +26 -0
- data/rake/dependencies.rb +76 -0
- data/rake/helpers.rb +434 -0
- data/rake/hg.rb +261 -0
- data/rake/manual.rb +782 -0
- data/rake/packaging.rb +135 -0
- data/rake/publishing.rb +318 -0
- data/rake/rdoc.rb +30 -0
- data/rake/style.rb +62 -0
- data/rake/svn.rb +668 -0
- data/rake/testing.rb +187 -0
- data/rake/verifytask.rb +64 -0
- data/rake/win32.rb +190 -0
- data/spec/lib/constants.rb +93 -0
- data/spec/lib/helpers.rb +100 -0
- data/spec/treequel/branch_spec.rb +569 -0
- data/spec/treequel/branchcollection_spec.rb +213 -0
- data/spec/treequel/branchset_spec.rb +376 -0
- data/spec/treequel/directory_spec.rb +487 -0
- data/spec/treequel/filter_spec.rb +482 -0
- data/spec/treequel/mixins_spec.rb +330 -0
- data/spec/treequel/schema/attributetype_spec.rb +237 -0
- data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
- data/spec/treequel/schema/matchingrule_spec.rb +158 -0
- data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
- data/spec/treequel/schema/objectclass_spec.rb +262 -0
- data/spec/treequel/schema_spec.rb +118 -0
- data/spec/treequel/utils_spec.rb +49 -0
- data/spec/treequel_spec.rb +179 -0
- metadata +169 -0
@@ -0,0 +1,541 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
require 'ldap'
|
6
|
+
require 'ldap/schema'
|
7
|
+
|
8
|
+
require 'treequel'
|
9
|
+
require 'treequel/schema'
|
10
|
+
require 'treequel/mixins'
|
11
|
+
require 'treequel/constants'
|
12
|
+
|
13
|
+
|
14
|
+
# The object in Treequel that represents a connection to a directory, the
|
15
|
+
# binding to that directory, and the base from which all DNs start.
|
16
|
+
#
|
17
|
+
# == Authors
|
18
|
+
#
|
19
|
+
# * Michael Granger <ged@FaerieMUD.org>
|
20
|
+
# * Mahlon E. Smith <mahlon@martini.nu>
|
21
|
+
#
|
22
|
+
# :include: LICENSE
|
23
|
+
#
|
24
|
+
#--
|
25
|
+
#
|
26
|
+
# Please see the file LICENSE in the base directory for licensing details.
|
27
|
+
#
|
28
|
+
class Treequel::Directory
|
29
|
+
include Treequel::Loggable,
|
30
|
+
Treequel::Constants,
|
31
|
+
Treequel::HashUtilities
|
32
|
+
|
33
|
+
extend Treequel::Delegation
|
34
|
+
|
35
|
+
# The default directory options
|
36
|
+
DEFAULT_OPTIONS = {
|
37
|
+
:host => 'localhost',
|
38
|
+
:port => LDAP::LDAP_PORT,
|
39
|
+
:connect_type => :tls,
|
40
|
+
:base_dn => nil,
|
41
|
+
:bind_dn => nil,
|
42
|
+
:pass => nil
|
43
|
+
}
|
44
|
+
|
45
|
+
# Default mapping of SYNTAX OIDs to conversions. See #add_syntax_mapping for more
|
46
|
+
# information on what a valid conversion is.
|
47
|
+
DEFAULT_SYNTAX_MAPPING = {
|
48
|
+
OIDS::BIT_STRING_SYNTAX => lambda {|bs| bs[0..-1].to_i(2) },
|
49
|
+
OIDS::BOOLEAN_SYNTAX => { 'TRUE' => true, 'FALSE' => false },
|
50
|
+
OIDS::GENERALIZED_TIME_SYNTAX => lambda {|string| Time.parse(string) },
|
51
|
+
OIDS::UTC_TIME_SYNTAX => lambda {|string| Time.parse(string) },
|
52
|
+
OIDS::INTEGER_SYNTAX => lambda {|string| Integer(string) },
|
53
|
+
}
|
54
|
+
|
55
|
+
# :NOTE: the docs for #search_ext2 lie. The method signature is actually:
|
56
|
+
# rb_scan_args (argc, argv, "39",
|
57
|
+
# &base, &scope, &filter, &attrs, &attrsonly,
|
58
|
+
# &serverctrls, &clientctrls, &sec, &usec, &limit,
|
59
|
+
# &s_attr, &s_proc)
|
60
|
+
|
61
|
+
# The order in which hash arguments should be extracted from Hash parameters to
|
62
|
+
# #search
|
63
|
+
SEARCH_PARAMETER_ORDER = [
|
64
|
+
:selectattrs,
|
65
|
+
:attrsonly,
|
66
|
+
:server_controls,
|
67
|
+
:client_controls,
|
68
|
+
:timeout_s,
|
69
|
+
:timeout_us,
|
70
|
+
:limit,
|
71
|
+
:sort_attribute,
|
72
|
+
:sort_func,
|
73
|
+
].freeze
|
74
|
+
|
75
|
+
# Default values to pass to LDAP::Conn#search_ext2; they'll be passed in the order
|
76
|
+
# specified by SEARCH_PARAMETER_ORDER.
|
77
|
+
SEARCH_DEFAULTS = {
|
78
|
+
:selectattrs => ['*'],
|
79
|
+
:attrsonly => false,
|
80
|
+
:server_controls => nil,
|
81
|
+
:client_controls => nil,
|
82
|
+
:timeout => 0,
|
83
|
+
:limit => 0,
|
84
|
+
:sortby => nil,
|
85
|
+
}.freeze
|
86
|
+
|
87
|
+
|
88
|
+
require 'treequel/branch'
|
89
|
+
|
90
|
+
# The methods that get delegated to the directory's #base branch.
|
91
|
+
DELEGATED_BRANCH_METHODS =
|
92
|
+
Treequel::Branch.instance_methods(false).collect {|m| m.to_sym }
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
#################################################################
|
97
|
+
### C L A S S M E T H O D S
|
98
|
+
#################################################################
|
99
|
+
|
100
|
+
### Create a new Treequel::Directory with the given +options+. Options is a hash with one
|
101
|
+
### or more of the following key-value pairs:
|
102
|
+
###
|
103
|
+
### [:host]
|
104
|
+
### The LDAP host to connect to.
|
105
|
+
### [:port]
|
106
|
+
### The port to connect to.
|
107
|
+
### [:connect_type]
|
108
|
+
### The type of connection to establish. Must be one of +:plain+, +:tls+, or +:ssl+.
|
109
|
+
### [:base_dn]
|
110
|
+
### The base DN of the directory.
|
111
|
+
### [:bind_dn]
|
112
|
+
### The DN of the user to bind as.
|
113
|
+
### [:pass]
|
114
|
+
### The password to use when binding.
|
115
|
+
def initialize( options={} )
|
116
|
+
options = DEFAULT_OPTIONS.merge( options )
|
117
|
+
|
118
|
+
@host = options[:host]
|
119
|
+
@port = options[:port]
|
120
|
+
@connect_type = options[:connect_type]
|
121
|
+
|
122
|
+
@conn = nil
|
123
|
+
@bound_as = nil
|
124
|
+
|
125
|
+
@base_dn = options[:base_dn] || self.get_default_base_dn
|
126
|
+
@syntax_mapping = DEFAULT_SYNTAX_MAPPING.dup
|
127
|
+
|
128
|
+
@base = nil
|
129
|
+
|
130
|
+
# Immediately bind if credentials are passed to the initializer.
|
131
|
+
if ( options[:bind_dn] && options[:pass] )
|
132
|
+
self.bind( options[:bind_dn], options[:pass] )
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
######
|
138
|
+
public
|
139
|
+
######
|
140
|
+
|
141
|
+
# Delegate some methods to the #base Branch.
|
142
|
+
def_method_delegators :base, *DELEGATED_BRANCH_METHODS
|
143
|
+
|
144
|
+
# The host to connect to.
|
145
|
+
attr_accessor :host
|
146
|
+
|
147
|
+
# The port to connect to.
|
148
|
+
attr_accessor :port
|
149
|
+
|
150
|
+
# The type of connection to establish
|
151
|
+
attr_accessor :connect_type
|
152
|
+
|
153
|
+
# The base DN of the directory
|
154
|
+
attr_accessor :base_dn
|
155
|
+
|
156
|
+
|
157
|
+
### Fetch the Branch for the base node of the directory.
|
158
|
+
def base
|
159
|
+
return @base ||= Treequel::Branch.new( self, self.base_dn )
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
### Returns a string that describes the directory
|
164
|
+
def to_s
|
165
|
+
return "%s:%d (%s, %s, %s)" % [
|
166
|
+
self.host,
|
167
|
+
self.port,
|
168
|
+
self.base_dn,
|
169
|
+
self.connect_type,
|
170
|
+
self.bound? ? @bound_as : 'anonymous'
|
171
|
+
]
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
### Return a human-readable representation of the object suitable for debugging
|
176
|
+
def inspect
|
177
|
+
return %{#<%s:0x%0x %s:%d (%s) base_dn=%p, bound as=%s, schema=%s>} % [
|
178
|
+
self.class.name,
|
179
|
+
self.object_id / 2,
|
180
|
+
self.host,
|
181
|
+
self.port,
|
182
|
+
@conn ? "connected" : "not connected",
|
183
|
+
self.base_dn,
|
184
|
+
@bound_as ? @bound_as.dump : "anonymous",
|
185
|
+
@schema ? @schema.inspect : "(schema not loaded)",
|
186
|
+
]
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
### Return the LDAP::Conn object associated with this directory, creating it with the
|
191
|
+
### current options if necessary.
|
192
|
+
def conn
|
193
|
+
return @conn ||= self.connect
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
### Return the URI object that corresponds to the directory.
|
198
|
+
def uri
|
199
|
+
uri_parts = {
|
200
|
+
:scheme => self.connect_type == :ssl ? 'ldaps' : 'ldap',
|
201
|
+
:host => self.host,
|
202
|
+
:port => self.port,
|
203
|
+
:dn => '/' + self.base_dn
|
204
|
+
}
|
205
|
+
|
206
|
+
return URI::LDAP.build( uri_parts )
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
### Bind as the specified +user_dn+ and +password+. If the optional +block+ is given,
|
211
|
+
### it will be executed with the receiver bound, then returned to its previous state when
|
212
|
+
### the block exits.
|
213
|
+
def bind( user_dn, password )
|
214
|
+
user_dn = user_dn.dn if user_dn.respond_to?( :dn )
|
215
|
+
|
216
|
+
self.log.info "Binding with connection %p as: %s" % [ self.conn, user_dn ]
|
217
|
+
self.conn.bind( user_dn.to_s, password )
|
218
|
+
@bound_as = user_dn.to_s
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
### Execute the provided +block+ after binding as +user_dn+ with the given +password+. After
|
223
|
+
### the block returns, the original binding (if any) will be restored.
|
224
|
+
def bound_as( user_dn, password )
|
225
|
+
raise LocalJumpError, "no block given" unless block_given?
|
226
|
+
previous_bind_dn = @bound_as
|
227
|
+
self.with_duplicate_conn do
|
228
|
+
self.bind( user_dn, password )
|
229
|
+
yield
|
230
|
+
end
|
231
|
+
ensure
|
232
|
+
@bound_as = previous_bind_dn
|
233
|
+
end
|
234
|
+
|
235
|
+
|
236
|
+
### Returns +true+ if the directory's connection has already established a binding.
|
237
|
+
def bound?
|
238
|
+
return self.conn.bound?
|
239
|
+
end
|
240
|
+
alias_method :is_bound?, :bound?
|
241
|
+
|
242
|
+
|
243
|
+
### Ensure that the the receiver's connection is unbound.
|
244
|
+
def unbind
|
245
|
+
if @conn.bound?
|
246
|
+
old_conn = @conn
|
247
|
+
@conn = old_conn.dup
|
248
|
+
old_conn.unbind
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
### Return the RDN string to the given +dn+ from the base of the directory.
|
254
|
+
def rdn_to( dn )
|
255
|
+
base_re = Regexp.new( ',' + Regexp.quote(self.base_dn) + '$' )
|
256
|
+
return dn.to_s.sub( base_re, '' )
|
257
|
+
end
|
258
|
+
|
259
|
+
|
260
|
+
### Given a Treequel::Branch object, find its corresponding LDAP::Entry and return
|
261
|
+
### it.
|
262
|
+
def get_entry( branch )
|
263
|
+
self.log.debug "Looking up entry for %p" % [ branch.dn ]
|
264
|
+
return self.conn.search_ext2( branch.dn, SCOPE[:base], '(objectClass=*)' ).first
|
265
|
+
rescue LDAP::ResultError => err
|
266
|
+
self.log.info " search for %p failed: %s" % [ branch.dn, err.message ]
|
267
|
+
return nil
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
### Given a Treequel::Branch object, find its corresponding LDAP::Entry and return
|
272
|
+
### it with its operational attributes (http://tools.ietf.org/html/rfc4512#section-3.4)
|
273
|
+
### included.
|
274
|
+
def get_extended_entry( branch )
|
275
|
+
self.log.debug "Looking up entry (with operational attributes) for %p" % [ branch.dn ]
|
276
|
+
return self.conn.search_ext2( branch.dn, SCOPE[:base], '(objectClass=*)', %w[* +] ).first
|
277
|
+
rescue LDAP::ResultError => err
|
278
|
+
self.log.info " search for %p failed: %s" % [ branch.dn, err.message ]
|
279
|
+
return nil
|
280
|
+
end
|
281
|
+
|
282
|
+
|
283
|
+
### Fetch the schema from the server.
|
284
|
+
def schema
|
285
|
+
unless @schema
|
286
|
+
schemahash = self.conn.schema
|
287
|
+
@schema = Treequel::Schema.new( schemahash )
|
288
|
+
end
|
289
|
+
|
290
|
+
return @schema
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
### Perform a +scope+ search at +base+ using the specified +filter+. The +scope+ argument
|
295
|
+
### can be one of +:onelevel+, +:base+, or +:subtree+. Results will be returned as instances
|
296
|
+
### of the given +collectclass+.
|
297
|
+
def search( base, scope=:subtree, filter='(objectClass=*)', parameters={} )
|
298
|
+
collectclass = nil
|
299
|
+
|
300
|
+
# If the base argument is an object whose class knows how to create instances of itself
|
301
|
+
# from an LDAP::Entry, use it instead of Treequel::Branch to wrap results
|
302
|
+
if parameters.key?( :results_class )
|
303
|
+
collectclass = parameters.delete( :results_class )
|
304
|
+
else
|
305
|
+
collectclass = base.class.respond_to?( :new_from_entry ) ? base.class : Treequel::Branch
|
306
|
+
end
|
307
|
+
|
308
|
+
# Format the arguments in the way #search_ext2 expects them
|
309
|
+
base, scope, filter, searchparams =
|
310
|
+
self.normalize_search_parameters( base, scope, filter, parameters )
|
311
|
+
|
312
|
+
# Unwrap the search parameters from the hash in the correct order
|
313
|
+
self.log.debug {
|
314
|
+
attrlist = SEARCH_PARAMETER_ORDER.inject([]) do |list, param|
|
315
|
+
list << "%s: %p" % [ param, searchparams[param] ]
|
316
|
+
end
|
317
|
+
"searching with base: %p, scope: %p, filter: %p, %s" %
|
318
|
+
[ base, scope, filter, attrlist.join(', ') ]
|
319
|
+
}
|
320
|
+
parameters = searchparams.values_at( *SEARCH_PARAMETER_ORDER )
|
321
|
+
|
322
|
+
# Wrap each result in the class derived from the 'base' argument
|
323
|
+
self.log.debug "Searching via search_ext2 with arguments: %p" % [[
|
324
|
+
base, scope, filter, *parameters
|
325
|
+
]]
|
326
|
+
if block_given?
|
327
|
+
self.conn.search_ext2( base, scope, filter, *parameters ).each do |entry|
|
328
|
+
yield collectclass.new_from_entry( entry, self )
|
329
|
+
end
|
330
|
+
else
|
331
|
+
return self.conn.search_ext2( base, scope, filter, *parameters ).
|
332
|
+
collect {|entry| collectclass.new_from_entry(entry, self) }
|
333
|
+
end
|
334
|
+
|
335
|
+
rescue RuntimeError => err
|
336
|
+
conn = self.conn
|
337
|
+
|
338
|
+
# The LDAP library raises a plain RuntimeError with an incorrect message if the
|
339
|
+
# connection goes away, so it's caught here to rewrap it
|
340
|
+
case err.message
|
341
|
+
when /no result returned by search/i
|
342
|
+
raise LDAP::ResultError.new( LDAP.err2string(conn.err) )
|
343
|
+
else
|
344
|
+
raise
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
### Modify the entry specified by the given +dn+ with the specified +mods+, which can be
|
350
|
+
### either an Array of LDAP::Mod objects or a Hash of attribute/value pairs.
|
351
|
+
def modify( branch, mods )
|
352
|
+
normattrs = self.normalize_attributes( mods )
|
353
|
+
self.log.debug "Modifying %s with attributes: %p" % [ branch.dn, normattrs ]
|
354
|
+
self.conn.modify( branch.dn, normattrs )
|
355
|
+
end
|
356
|
+
|
357
|
+
|
358
|
+
### Delete the entry specified by the given +branch+.
|
359
|
+
def delete( branch )
|
360
|
+
self.log.info "Deleting %s from the directory." % [ branch ]
|
361
|
+
self.conn.delete( branch.dn )
|
362
|
+
end
|
363
|
+
|
364
|
+
|
365
|
+
### Create the entry for the given +branch+, setting its attributes to +newattrs+.
|
366
|
+
def create( branch, newattrs={} )
|
367
|
+
newdn = branch.dn
|
368
|
+
schema = self.schema
|
369
|
+
|
370
|
+
# Merge RDN attributes with existing ones, combining any that exist in both
|
371
|
+
self.log.debug "Smushing rdn attributes %p into %p" % [ branch.rdn_attributes, newdn ]
|
372
|
+
newattrs.merge!( branch.rdn_attributes ) do |key, *values|
|
373
|
+
values.flatten
|
374
|
+
end
|
375
|
+
|
376
|
+
normattrs = self.normalize_attributes( newattrs )
|
377
|
+
raise ArgumentError, "Can't create an entry with no objectClasses" unless
|
378
|
+
normattrs.key?( 'objectClass' )
|
379
|
+
normattrs['objectClass'].each do |oc|
|
380
|
+
raise ArgumentError, "No such objectClass #{oc.inspect}" unless
|
381
|
+
schema.object_classes.key?(oc.to_sym)
|
382
|
+
end
|
383
|
+
raise ArgumentError, "Can't create an entry with no structural objectClass" unless
|
384
|
+
normattrs['objectClass'].any? {|oc| schema.object_classes[oc.to_sym].structural? }
|
385
|
+
|
386
|
+
self.log.debug "Creating an entry at %s with the attributes: %p" % [ newdn, normattrs ]
|
387
|
+
self.conn.add( newdn, normattrs )
|
388
|
+
|
389
|
+
return true
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
### Move the entry from the specified +branch+ to the new entry specified by
|
394
|
+
### +newdn+. Returns the (moved) branch object.
|
395
|
+
def move( branch, newdn )
|
396
|
+
source_rdn, source_parent_dn = branch.split_dn( 2 )
|
397
|
+
new_rdn, new_parent_dn = newdn.split( /\s*,\s*/, 2 )
|
398
|
+
|
399
|
+
if new_parent_dn.nil?
|
400
|
+
new_parent_dn = source_parent_dn
|
401
|
+
newdn = [new_rdn, new_parent_dn].join(',')
|
402
|
+
end
|
403
|
+
|
404
|
+
if new_parent_dn != source_parent_dn
|
405
|
+
raise Treequel::Error,
|
406
|
+
"can't (yet) move an entry to a new parent"
|
407
|
+
end
|
408
|
+
|
409
|
+
self.log.debug "Modrdn (move): %p -> %p within %p" % [ source_rdn, new_rdn, source_parent_dn ]
|
410
|
+
|
411
|
+
self.conn.modrdn( branch.dn, new_rdn, true )
|
412
|
+
branch.dn = newdn
|
413
|
+
end
|
414
|
+
|
415
|
+
|
416
|
+
### Add +conversion+ mapping for the specified +oid+. A conversion is any object that
|
417
|
+
### responds to #[] with a String argument(e.g., Proc, Method, Hash); the argument is
|
418
|
+
### the raw value String returned from the LDAP entry, and it should return the
|
419
|
+
### converted value. Adding a mapping with a nil +conversion+ effectively clears it.
|
420
|
+
def add_syntax_mapping( oid, conversion=nil )
|
421
|
+
conversion = Proc.new if block_given?
|
422
|
+
@syntax_mapping[ oid ] = conversion
|
423
|
+
end
|
424
|
+
|
425
|
+
|
426
|
+
### Map the specified +value+ to its Ruby datatype if one is registered for the given
|
427
|
+
### syntax +oid+. If there is no conversion registered, just return the +value+ as-is.
|
428
|
+
def convert_syntax_value( oid, value )
|
429
|
+
self.log.debug "Converting value %p using the syntax rule for %p" % [ value, oid ]
|
430
|
+
unless conversion = @syntax_mapping[ oid ]
|
431
|
+
self.log.debug " ...no conversion, returning it as-is."
|
432
|
+
return value
|
433
|
+
end
|
434
|
+
|
435
|
+
self.log.debug " ...found conversion: %p" % [ conversion ]
|
436
|
+
return conversion[ value ]
|
437
|
+
end
|
438
|
+
|
439
|
+
|
440
|
+
|
441
|
+
#########
|
442
|
+
protected
|
443
|
+
#########
|
444
|
+
|
445
|
+
### Delegate attribute/value calls on the directory itself to the directory's #base Branch.
|
446
|
+
def method_missing( attribute, *args )
|
447
|
+
return self.base.send( attribute, *args )
|
448
|
+
end
|
449
|
+
|
450
|
+
|
451
|
+
### Create a new LDAP::Conn object with the current host, port, and connect_type
|
452
|
+
### and return it.
|
453
|
+
def connect
|
454
|
+
conn = nil
|
455
|
+
|
456
|
+
case @connect_type
|
457
|
+
when :tls
|
458
|
+
self.log.debug "Connecting using TLS to %s:%d" % [ @host, @port ]
|
459
|
+
conn = LDAP::SSLConn.new( @host, @port, true )
|
460
|
+
when :ssl
|
461
|
+
self.log.debug "Connecting using SSL to %s:%d" % [ @host, @port ]
|
462
|
+
conn = LDAP::SSLConn.new( host, port )
|
463
|
+
else
|
464
|
+
self.log.debug "Connecting using an unencrypted connection to %s:%d" % [ @host, @port ]
|
465
|
+
conn = LDAP::Conn.new( host, port )
|
466
|
+
end
|
467
|
+
|
468
|
+
conn.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
|
469
|
+
conn.set_option( LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_OFF )
|
470
|
+
|
471
|
+
return conn
|
472
|
+
end
|
473
|
+
|
474
|
+
|
475
|
+
### Fetch the default base dn for the server from the server's Root DSE.
|
476
|
+
def get_default_base_dn
|
477
|
+
dse = self.conn.root_dse
|
478
|
+
return '' if dse.nil? || dse.empty?
|
479
|
+
return dse.first['namingContexts'].first
|
480
|
+
end
|
481
|
+
|
482
|
+
|
483
|
+
### Execute a block with a copy of the current connection, restoring the original
|
484
|
+
### after the block returns.
|
485
|
+
def with_duplicate_conn
|
486
|
+
original_conn = self.conn
|
487
|
+
@conn = original_conn.dup
|
488
|
+
self.log.info "Executing with %p, a copy of connection %p" % [ @conn, original_conn ]
|
489
|
+
yield
|
490
|
+
ensure
|
491
|
+
self.log.info " restoring original connection %p." % [ original_conn ]
|
492
|
+
@conn = original_conn
|
493
|
+
end
|
494
|
+
|
495
|
+
|
496
|
+
### Normalize the attributes in +hash+ to be of the form expected by the
|
497
|
+
### LDAP library (i.e., keys as Strings, values as Arrays of Strings)
|
498
|
+
def normalize_attributes( hash )
|
499
|
+
normhash = {}
|
500
|
+
hash.each do |key,val|
|
501
|
+
val = [ val ] unless val.is_a?( Array )
|
502
|
+
val.collect! {|obj| obj.to_s }
|
503
|
+
|
504
|
+
normhash[ key.to_s ] = val
|
505
|
+
end
|
506
|
+
|
507
|
+
normhash.delete( 'dn' )
|
508
|
+
|
509
|
+
return normhash
|
510
|
+
end
|
511
|
+
|
512
|
+
|
513
|
+
### Normalize the parameters to the #search method into the format expected by
|
514
|
+
### the LDAP::Conn#Search_ext2 method and return them as a Hash.
|
515
|
+
def normalize_search_parameters( base, scope, filter, parameters )
|
516
|
+
search_paramhash = SEARCH_DEFAULTS.merge( parameters )
|
517
|
+
|
518
|
+
# Use the DN of the base object if it's an object that knows what a DN is
|
519
|
+
base = base.dn if base.respond_to?( :dn )
|
520
|
+
scope = SCOPE[scope.to_sym] if scope.respond_to?( :to_sym ) && SCOPE.key?( scope.to_sym )
|
521
|
+
filter = filter.to_s
|
522
|
+
|
523
|
+
# Split seconds and microseconds from the timeout value, convert the
|
524
|
+
# fractional part to µsec
|
525
|
+
timeout = search_paramhash.delete( :timeout ) || 0
|
526
|
+
search_paramhash[:timeout_s] = timeout.truncate
|
527
|
+
search_paramhash[:timeout_us] = Integer((timeout - timeout.truncate) * 1_000_000)
|
528
|
+
|
529
|
+
### Sorting in Ruby-LDAP is not significantly more useful than just sorting
|
530
|
+
### the returned entries from Ruby, as it happens client-side anyway (i.e., entries
|
531
|
+
### are still returned from the server in arbitrary/insertion order, and then the client
|
532
|
+
### sorts those
|
533
|
+
search_paramhash[:sort_func] = nil
|
534
|
+
search_paramhash[:sort_attribute] = ''
|
535
|
+
|
536
|
+
return base, scope, filter, search_paramhash
|
537
|
+
end
|
538
|
+
|
539
|
+
end # class Treequel::Directory
|
540
|
+
|
541
|
+
|