treequel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|