ruby-ldap 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,50 @@
1
+ # Manipulation of LDAP control data.
2
+ #
3
+ #--
4
+ # $Id: control.rb,v 1.2 2005/02/28 05:02:25 ianmacd Exp $
5
+ #++
6
+ #
7
+ # Copyright (C) 2004 Ian Macdonald <ian@caliban.org>
8
+ #
9
+
10
+ module LDAP
11
+ class Control
12
+
13
+ require 'openssl'
14
+
15
+ # Take +vals+, produce an Array of values in ASN.1 format and then
16
+ # convert the Array to DER.
17
+ #
18
+ def Control.encode( *vals )
19
+ encoded_vals = []
20
+
21
+ vals.each do |val|
22
+ encoded_vals <<
23
+ case val
24
+ when Integer
25
+ OpenSSL::ASN1::Integer( val )
26
+ when String
27
+ OpenSSL::ASN1::OctetString.new( val )
28
+ else
29
+ # What other types may exist?
30
+ end
31
+ end
32
+
33
+ OpenSSL::ASN1::Sequence.new( encoded_vals ).to_der
34
+ end
35
+
36
+
37
+ # Take an Array of ASN.1 data and return an Array of decoded values.
38
+ #
39
+ def decode
40
+ values = []
41
+
42
+ OpenSSL::ASN1::decode( self.value ).value.each do |val|
43
+ values << val.value
44
+ end
45
+
46
+ values
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,569 @@
1
+ # Manipulation of LDIF data.
2
+ #
3
+ #--
4
+ # $Id: ldif.rb,v 1.11 2005/03/03 01:32:07 ianmacd Exp $
5
+ #++
6
+ #
7
+ # Copyright (C) 2005 Ian Macdonald <ian@caliban.org>
8
+ #
9
+
10
+ module LDAP
11
+
12
+ # Record objects are embodiments of LDAP operations. They possess a DN,
13
+ # a change type (*LDAP_MOD_ADD*, *LDAP_MOD_DELETE* or *LDAP_MOD_REPLACE*
14
+ # [any of which can be logically AND'ed with *LDAP_MOD_BVALUES*]), a hash of
15
+ # attributes and value arrays, a hash of modification operations (useful
16
+ # only when the change type is *LDAP_MOD_REPLACE*) and an array of
17
+ # LDAP controls.
18
+ #
19
+ # The Record class's primary use is as a transitional medium for LDIF
20
+ # operations parsed by the LDAP::LDIF module. You are unlikely to want to
21
+ # use it in application code.
22
+ #
23
+ class Record
24
+ attr_reader :dn, :change_type, :attrs, :mods, :controls
25
+
26
+ def initialize(dn, change_type, attrs, mods=nil, ctls=nil)
27
+ @dn = dn
28
+ @change_type = change_type
29
+ @attrs = attrs
30
+ @mods = mods
31
+ @controls = ctls
32
+ end
33
+
34
+
35
+ # Send the operation embodied in the Record object to the LDAP::Conn
36
+ # object specified in +conn+.
37
+ #
38
+ def send( conn )
39
+ if @change_type == :MODRDN
40
+ # TODO: How do we deal with 'newsuperior'?
41
+ # The LDAP API's ldap_modrdn2_s() function doesn't seem to use it.
42
+ return conn.modrdn( @dn, @attrs['newrdn'], @attrs['deleteoldrdn'] )
43
+ end
44
+
45
+ # Mask out the LDAP_MOD_BVALUES bit, as it's irrelevant here.
46
+ case @change_type & ~LDAP_MOD_BVALUES
47
+ when LDAP_MOD_ADD
48
+ @controls == [] ? conn.add( @dn, @attrs ) :
49
+ conn.add_ext( @dn, @attrs, @controls, [] )
50
+ when LDAP_MOD_DELETE
51
+ @controls == [] ? conn.delete( @dn ) :
52
+ conn.delete_ext( @dn, @controls, [] )
53
+ when LDAP_MOD_REPLACE
54
+ @controls == [] ? conn.modify( @dn, @mods ) :
55
+ conn.modify_ext( @dn, @mods, @controls, [] )
56
+ end
57
+
58
+ self
59
+ end
60
+
61
+
62
+ # Remove common operational attributes from a Record object. This is
63
+ # useful if you have Record objects formed from LDIF data that contained
64
+ # operational attributes. Using LDAP::Record#send to send such an object
65
+ # to an LDAP server is likely to meet with an exception unless the data is
66
+ # first cleaned.
67
+ #
68
+ # In addition, attributes with duplicate values are pruned, as this can
69
+ # also result in an exception.
70
+ #
71
+ def clean
72
+
73
+ # TODO: These operational attributes are those commonly used by
74
+ # OpenLDAP 2.2. Others should probably be supported.
75
+ #
76
+ %w[ creatorsname createtimestamp modifiersname modifytimestamp
77
+ entrycsn entryuuid structuralobjectclass ].each do |attr|
78
+ @attrs.delete( attr )
79
+ end
80
+
81
+ # Clean out duplicate attribute values.
82
+ @attrs.each_key { |k| @attrs[k].uniq! }
83
+
84
+ self
85
+ end
86
+
87
+ end
88
+
89
+
90
+ # This module provides the ability to process LDIF entries and files.
91
+ #
92
+ module LDIF
93
+ LINE_LENGTH = 77
94
+
95
+ private
96
+
97
+ class Entry < String; end
98
+ class Mod < String; end
99
+ class LDIFError < LDAP::Error; end
100
+
101
+
102
+ # return *true* if +str+ contains a character with an ASCII value > 127 or
103
+ # a NUL, LF or CR. Otherwise, *false* is returned.
104
+ #
105
+ def LDIF.unsafe_char?( str )
106
+ # This could be written as a single regex, but this is faster.
107
+ str =~ /^[ :]/ || str =~ /[\x00-\x1f\x7f-\xff]/
108
+ end
109
+
110
+
111
+ # Perform Base64 decoding of +str+. If +concat+ is *true*, LF characters
112
+ # are stripped.
113
+ #
114
+ def LDIF.base64_encode( str, concat=false )
115
+ str = [ str ].pack( 'm' )
116
+ str.gsub!( /\n/, '' ) if concat
117
+ str
118
+ end
119
+
120
+
121
+ # Perform Base64 encoding of +str+.
122
+ #
123
+ def LDIF.base64_decode( str )
124
+ str.unpack( 'm*' )[0]
125
+ end
126
+
127
+
128
+ # Read a file from the URL +url+. At this time, the only type of URL
129
+ # supported is the +file://+ URL.
130
+ #
131
+ def LDIF.read_file( url )
132
+ unless url.sub!( %r(^file://), '' )
133
+ raise ArgumentError, "Bad external file reference: #{url}"
134
+ end
135
+
136
+ # Slurp an external file.
137
+ # TODO: Support other URL types in the future.
138
+ File.open( url ).readlines( nil )[0]
139
+ end
140
+
141
+
142
+ # This converts an attribute and array of values to LDIF.
143
+ #
144
+ def LDIF.to_ldif( attr, vals )
145
+ ldif = ''
146
+
147
+ vals.each do |val|
148
+ sep = ':'
149
+ if unsafe_char?( val )
150
+ sep = '::'
151
+ val = base64_encode( val, true )
152
+ end
153
+
154
+ firstline_len = LINE_LENGTH - ( "%s%s " % [ attr, sep ] ).length
155
+ ldif << "%s%s %s\n" % [ attr, sep, val.slice!( 0..firstline_len ) ]
156
+
157
+ while val.length > 0
158
+ ldif << " %s\n" % val.slice!( 0..LINE_LENGTH - 1 )
159
+ end
160
+ end
161
+
162
+ ldif
163
+
164
+ end
165
+
166
+
167
+ public
168
+
169
+
170
+ # Parse the LDIF entry contained in +lines+ and return an LDAP::Record
171
+ # object. +lines+ should be an object that responds to each, such as a
172
+ # string or an array of lines, separated by \n characters.
173
+ #
174
+ def LDIF.parse_entry( lines )
175
+ header = true
176
+ comment = false
177
+ change_type = nil
178
+ sep = nil
179
+ attr = nil
180
+ bvalues = []
181
+ controls = nil
182
+ hash = {}
183
+ mods = {}
184
+ mod_type = nil
185
+
186
+ lines.each do |line|
187
+ # Skip (continued) comments.
188
+ if line =~ /^#/ || ( comment && line[0..0] == ' ' )
189
+ comment = true
190
+ next
191
+ end
192
+
193
+ # Skip blank lines.
194
+ next if line =~ /^$/
195
+
196
+ # Reset mod type if this entry has more than one mod to make.
197
+ # A '-' continuation is only valid if we've already had a
198
+ # 'changetype: modify' line.
199
+ if line =~ /^-$/ && change_type == LDAP_MOD_REPLACE
200
+ next
201
+ end
202
+
203
+ line.chomp!
204
+
205
+ # N.B. Attributes and values can be separated by one or two colons,
206
+ # or one colon and a '<'. Either of these is then followed by zero
207
+ # or one spaces.
208
+ if md = line.match( /^[^ ].*?((:[:<]?) ?)/ )
209
+
210
+ # If previous value was Base64-encoded and is not continued,
211
+ # we need to decode it now.
212
+ if sep == '::'
213
+ if mod_type
214
+ mods[mod_type][attr][-1] =
215
+ base64_decode( mods[mod_type][attr][-1] )
216
+ bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
217
+ else
218
+ hash[attr][-1] = base64_decode( hash[attr][-1] )
219
+ bvalues << attr if unsafe_char?( hash[attr][-1] )
220
+ end
221
+
222
+ end
223
+
224
+ # Found a attr/value line.
225
+ attr, val = line.split( md[1], 2 )
226
+ attr.downcase!
227
+
228
+ # Attribute must be ldap-oid / (ALPHA *(attr-type-chars))
229
+ if attr !~ /^(?:(?:\d+\.)*\d+|[[:alnum:]-]+)(?:;[[:alnum:]-]+)*$/
230
+ raise LDIFError, "Invalid attribute: #{attr}"
231
+ end
232
+
233
+ if attr == 'dn'
234
+ header = false
235
+ change_type = nil
236
+ controls = []
237
+ end
238
+ sep = md[2]
239
+
240
+ val = read_file( val ) if sep == ':<'
241
+
242
+ case attr
243
+ when 'version'
244
+ # Check the LDIF version.
245
+ if header
246
+ if val != '1'
247
+ raise LDIFError, "Unsupported LDIF version: #{val}"
248
+ else
249
+ header = false
250
+ next
251
+ end
252
+ end
253
+
254
+ when 'changetype'
255
+ change_type = case val
256
+ when 'add' then LDAP_MOD_ADD
257
+ when 'delete' then LDAP_MOD_DELETE
258
+ when 'modify' then LDAP_MOD_REPLACE
259
+ when /^modr?dn$/ then :MODRDN
260
+ end
261
+
262
+ raise LDIFError, "Invalid change type: #{attr}" unless change_type
263
+
264
+ when 'add', 'delete', 'replace'
265
+ unless change_type == LDAP_MOD_REPLACE
266
+ raise LDIFError, "Cannot #{attr} here."
267
+ end
268
+
269
+ mod_type = case attr
270
+ when 'add' then LDAP_MOD_ADD
271
+ when 'delete' then LDAP_MOD_DELETE
272
+ when 'replace' then LDAP_MOD_REPLACE
273
+ end
274
+
275
+ mods[mod_type] ||= {}
276
+ mods[mod_type][val] ||= []
277
+
278
+ when 'control'
279
+
280
+ oid, criticality = val.split( / /, 2 )
281
+
282
+ unless oid =~ /(?:\d+\.)*\d+/
283
+ raise LDIFError, "Bad control OID: #{oid}"
284
+ end
285
+
286
+ if criticality
287
+ md = criticality.match( /(:[:<]?) ?/ )
288
+ ctl_sep = md[1] if md
289
+ criticality, value = criticality.split( /:[:<]? ?/, 2 )
290
+
291
+ if criticality !~ /^(?:true|false)$/
292
+ raise LDIFError, "Bad control criticality: #{criticality}"
293
+ end
294
+
295
+ # Convert 'true' or 'false'. to_boolean would be nice. :-)
296
+ criticality = eval( criticality )
297
+ end
298
+
299
+ if value
300
+ value = base64_decode( value ) if ctl_sep == '::'
301
+ value = read_file( value ) if ctl_sep == ':<'
302
+ value = Control.encode( value )
303
+ end
304
+
305
+ controls << Control.new( oid, value, criticality )
306
+ else
307
+
308
+ # Convert modrdn's deleteoldrdn from '1' to true, anything else
309
+ # to false. Should probably raise an exception if not '0' or '1'.
310
+ #
311
+ if change_type == :MODRDN && attr == 'deleteoldrdn'
312
+ val = val == '1' ? true : false
313
+ end
314
+
315
+ if change_type == LDAP_MOD_REPLACE
316
+ mods[mod_type][attr] << val
317
+ else
318
+ hash[attr] ||= []
319
+ hash[attr] << val
320
+ end
321
+
322
+ comment = false
323
+
324
+ # Make a note of this attribute if value is binary.
325
+ bvalues << attr if unsafe_char?( val )
326
+ end
327
+
328
+ else
329
+
330
+ # Check last line's separator: if not a binary value, the
331
+ # continuation line must be indented. If a comment makes it this
332
+ # far, that's also an error.
333
+ #
334
+ if sep == ':' && line[0..0] != ' ' || comment
335
+ raise LDIFError, "Improperly continued line: #{line}"
336
+ end
337
+
338
+ # OK; this is a valid continuation line.
339
+
340
+ # Append line except for initial space.
341
+ line[0] = '' if line[0..0] == ' '
342
+
343
+ if change_type == LDAP_MOD_REPLACE
344
+ # Append to last value of current mod type.
345
+ mods[mod_type][attr][-1] << line
346
+ else
347
+ # Append to last value.
348
+ hash[attr][-1] << line
349
+ end
350
+ end
351
+
352
+ end
353
+
354
+ # If last value in LDIF entry was Base64-encoded, we need to decode
355
+ # it now.
356
+ if sep == '::'
357
+ if mod_type
358
+ mods[mod_type][attr][-1] =
359
+ base64_decode( mods[mod_type][attr][-1] )
360
+ bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
361
+ else
362
+ hash[attr][-1] = base64_decode( hash[attr][-1] )
363
+ bvalues << attr if unsafe_char?( hash[attr][-1] )
364
+ end
365
+ end
366
+
367
+ # Remove and remember DN.
368
+ dn = hash.delete( 'dn' )[0]
369
+
370
+ # This doesn't really matter, but let's be anal about it, because it's
371
+ # not an attribute and doesn't belong here.
372
+ bvalues.delete( 'dn' )
373
+
374
+ # If there's no change type, it's just plain LDIF data, so we'll treat
375
+ # it like an addition.
376
+ change_type ||= LDAP_MOD_ADD
377
+
378
+ case change_type
379
+ when LDAP_MOD_ADD
380
+
381
+ mods[LDAP_MOD_ADD] = []
382
+
383
+ hash.each do |attr_local, val|
384
+ if bvalues.include?( attr_local )
385
+ ct = LDAP_MOD_ADD | LDAP_MOD_BVALUES
386
+ else
387
+ ct = LDAP_MOD_ADD
388
+ end
389
+
390
+ mods[LDAP_MOD_ADD] << LDAP.mod( ct, attr_local, val )
391
+ end
392
+
393
+ when LDAP_MOD_DELETE
394
+
395
+ # Nothing to do.
396
+
397
+ when LDAP_MOD_REPLACE
398
+
399
+ raise LDIFError, "mods should not be empty" if mods == {}
400
+
401
+ new_mods = {}
402
+
403
+ mods.each do |mod_type_local,attrs|
404
+ attrs.each_key do |attr_local|
405
+ if bvalues.include?( attr_local )
406
+ mt = mod_type_local | LDAP_MOD_BVALUES
407
+ else
408
+ mt = mod_type_local
409
+ end
410
+
411
+ new_mods[mt] ||= {}
412
+ new_mods[mt][attr_local] = mods[mod_type_local][attr_local]
413
+ end
414
+ end
415
+
416
+ mods = new_mods
417
+
418
+ when :MODRDN
419
+
420
+ # Nothing to do.
421
+
422
+ end
423
+
424
+ Record.new( dn, change_type, hash, mods, controls )
425
+ end
426
+
427
+
428
+ # Open and parse a file containing LDIF entries. +file+ should be a string
429
+ # containing the path to the file. If +sort+ is true, the resulting array
430
+ # of LDAP::Record objects will be sorted on DN length, which can be useful
431
+ # to avoid a later attempt to process an entry whose parent does not yet
432
+ # exist. This can easily happen if your LDIF file is unordered, which is
433
+ # likely if it was produced with a tool such as <em>slapcat(8)</em>.
434
+ #
435
+ # If a block is given, each LDAP::Record object will be yielded to the
436
+ # block and *nil* will be returned instead of the array. This is much less
437
+ # memory-intensive when parsing a large LDIF file.
438
+ #
439
+ def LDIF.parse_file( file, sort=false ) # :yield: record
440
+
441
+ File.open( file ) do |f|
442
+ entries = []
443
+ entry = false
444
+ header = true
445
+ version = false
446
+
447
+ while line = f.gets
448
+
449
+ if line =~ /^dn:/
450
+ header = false
451
+
452
+ if entry && ! version
453
+ if block_given?
454
+ yield parse_entry( entry )
455
+ else
456
+ entries << parse_entry( entry )
457
+ end
458
+ end
459
+
460
+ if version
461
+ entry << line
462
+ version = false
463
+ else
464
+ entry = [ line ]
465
+ end
466
+
467
+ next
468
+ end
469
+
470
+ if header && line.downcase =~ /^version/
471
+ entry = [ line ]
472
+ version = true
473
+ next
474
+ end
475
+
476
+ entry << line
477
+ end
478
+
479
+ if block_given?
480
+ yield parse_entry( entry )
481
+ nil
482
+ else
483
+ entries << parse_entry( entry )
484
+
485
+ # Sort entries if sorting has been requested.
486
+ entries.sort! { |x,y| x.dn.length <=> y.dn.length } if sort
487
+ entries
488
+ end
489
+
490
+ end
491
+
492
+ end
493
+
494
+
495
+ # Given the DN, +dn+, convert a single LDAP::Mod or an array of
496
+ # LDAP::Mod objects, given in +mods+, to LDIF.
497
+ #
498
+ def LDIF.mods_to_ldif( dn, *mods )
499
+ ldif = "dn: %s\nchangetype: modify\n" % dn
500
+ plural = false
501
+
502
+ mods.flatten.each do |mod|
503
+ # TODO: Need to dynamically assemble this case statement to add
504
+ # OpenLDAP's increment change type, etc.
505
+ change_type = case mod.mod_op & ~LDAP_MOD_BVALUES
506
+ when LDAP_MOD_ADD then 'add'
507
+ when LDAP_MOD_DELETE then 'delete'
508
+ when LDAP_MOD_REPLACE then 'replace'
509
+ end
510
+
511
+ ldif << "-\n" if plural
512
+ ldif << LDIF.to_ldif( change_type, mod.mod_type )
513
+ ldif << LDIF.to_ldif( mod.mod_type, mod.mod_vals )
514
+
515
+ plural = true
516
+ end
517
+
518
+ LDIF::Mod.new( ldif )
519
+ end
520
+
521
+ end
522
+
523
+
524
+ class Entry
525
+
526
+ # Convert an LDAP::Entry to LDIF.
527
+ #
528
+ def to_ldif
529
+ ldif = "dn: %s\n" % get_dn
530
+
531
+ get_attributes.each do |attr|
532
+ get_values( attr ).each do |val|
533
+ ldif << LDIF.to_ldif( attr, [ val ] )
534
+ end
535
+ end
536
+
537
+ LDIF::Entry.new( ldif )
538
+ end
539
+
540
+ alias_method :to_s, :to_ldif
541
+ end
542
+
543
+
544
+ class Mod
545
+
546
+ # Convert an LDAP::Mod with the DN given in +dn+ to LDIF.
547
+ #
548
+ def to_ldif( dn )
549
+ ldif = "dn: %s\n" % dn
550
+
551
+ # TODO: Need to dynamically assemble this case statement to add
552
+ # OpenLDAP's increment change type, etc.
553
+ case mod_op & ~LDAP_MOD_BVALUES
554
+ when LDAP_MOD_ADD
555
+ ldif << "changetype: add\n"
556
+ when LDAP_MOD_DELETE
557
+ ldif << "changetype: delete\n"
558
+ when LDAP_MOD_REPLACE
559
+ return LDIF.mods_to_ldif( dn, self )
560
+ end
561
+
562
+ ldif << LDIF.to_ldif( mod_type, mod_vals )
563
+ LDIF::Mod.new( ldif )
564
+ end
565
+
566
+ alias_method :to_s, :to_ldif
567
+ end
568
+
569
+ end