ruby-activeldap-debug 0.5.5

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.
@@ -0,0 +1,1244 @@
1
+ # === ActiveLDAP - an OO-interface to LDAP objects inspired by ActiveRecord
2
+ # Author: Will Drewry <will@alum.bu.edu>
3
+ # License: See LICENSE and COPYING.txt
4
+ # Copyright 2004 Will Drewry <will@alum.bu.edu>
5
+ #
6
+ # == Summary
7
+ # ActiveLDAP lets you read and update LDAP entries in a completely object
8
+ # oriented fashion, even handling attributes with multiple names seamlessly.
9
+ # It was inspired by ActiveRecord so extending it to deal with custom
10
+ # LDAP schemas is as effortless as knowing the 'ou' of the objects, and the
11
+ # primary key. (fix this up some)
12
+ #
13
+ # == Example
14
+ # irb> require 'activeldap'
15
+ # > true
16
+ # irb> user = ActiveLDAP::User.new("drewry")
17
+ # > #<ActiveLDAP::User:0x402e...
18
+ # irb> user.cn
19
+ # > "foo"
20
+ # irb> user.commonname
21
+ # > "foo"
22
+ # irb> user.cn = "Will Drewry"
23
+ # > "Will Drewry"
24
+ # irb> user.cn
25
+ # > "Will Drewry"
26
+ # irb> user.validate
27
+ # > nil
28
+ # irb> user.write
29
+ #
30
+ #
31
+
32
+ require 'ldap'
33
+ require 'ldap/schema'
34
+ require 'log4r'
35
+
36
+ module ActiveLDAP
37
+ # OO-interface to LDAP assuming pam/nss_ldap-style comanization with Active specifics
38
+ # Each subclass does a ldapsearch for the matching entry.
39
+ # If no exact match, raise an error.
40
+ # If match, change all LDAP attributes in accessor attributes on the object.
41
+ # -- these are ACTUALLY populated from schema - see subschema.rb example
42
+ # -- @conn.schema().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}}
43
+ # -- extract objectClasses from match and populate
44
+ # Multiple entries become lists.
45
+ # If this isn't read-only then lists become multiple entries, etc.
46
+
47
+ # AttributeEmpty
48
+ #
49
+ # An exception raised when a required attribute is found to be empty
50
+ class AttributeEmpty < RuntimeError
51
+ end
52
+
53
+ # DeleteError
54
+ #
55
+ # An exception raised when an ActiveLDAP delete action fails
56
+ class DeleteError < RuntimeError
57
+ end
58
+
59
+ # WriteError
60
+ #
61
+ # An exception raised when an ActiveLDAP write action fails
62
+ class WriteError < RuntimeError
63
+ end
64
+
65
+ # AuthenticationError
66
+ #
67
+ # An exception raised when user authentication fails
68
+ class AuthenticationError < RuntimeError
69
+ end
70
+
71
+ # ConnectionError
72
+ #
73
+ # An exception raised when the LDAP conenction fails
74
+ class ConnectionError < RuntimeError
75
+ end
76
+
77
+ # ObjectClassError
78
+ #
79
+ # An exception raised when an objectClass is not defined in the schema
80
+ class ObjectClassError < RuntimeError
81
+ end
82
+
83
+
84
+ # Base
85
+ #
86
+ # Base is the primary class which contains all of the core
87
+ # ActiveLDAP functionality. It is meant to only ever be subclassed
88
+ # by extension classes.
89
+ class Base
90
+ # Parsed schema structures
91
+ attr_reader :must, :may
92
+ attr_accessor :logger
93
+
94
+ # All class-wide variables
95
+ @@config = nil # Container for current connection settings
96
+ @@schema = nil # LDAP server's schema
97
+ @@conn = nil # LDAP connection
98
+
99
+ # Driver generator
100
+ #
101
+ # TODO add type checking
102
+ # This let's you call this method to create top-level extension object. This
103
+ # is really just a proof of concept and has not truly useful purpose.
104
+ # example: Base.create_object(:class => "user", :dnattr => "uid", :classes => ['top'])
105
+ #
106
+ def Base.create_object(config={})
107
+ # Just upcase the first letter of the new class name
108
+ str = config[:class]
109
+ class_name = str[0].chr.upcase + str[1..-1]
110
+
111
+ attr = config[:dnattr] # "uid"
112
+ prefix = config[:base] # "ou=People"
113
+ # [ 'top', 'posixAccount' ]
114
+ classes_array = config[:classes] || []
115
+ # [ [ :groups, {:class_name => "Group", :foreign_key => "memberUid"}] ]
116
+ belongs_to_array = config[:belongs_to] || []
117
+ # [ [ :members, {:class_name => "User", :foreign_key => "uid", :local_key => "memberUid"}] ]
118
+ has_many_array = config[:has_many] || []
119
+
120
+ raise TypeError, ":objectclasses must be an array" unless classes_array.respond_to? :size
121
+ raise TypeError, ":belongs_to must be an array" unless belongs_to_array.respond_to? :size
122
+ raise TypeError, ":has_many must be an array" unless has_many_array.respond_to? :size
123
+
124
+ # Build classes array
125
+ classes = '['
126
+ classes_array.map! {|x| x = "'#{x}'"}
127
+ classes << classes_array.join(', ')
128
+ classes << ']'
129
+
130
+ # Build belongs_to
131
+ belongs_to = []
132
+ if belongs_to_array.size > 0
133
+ belongs_to_array.each do |bt|
134
+ line = [ "belongs_to :#{bt[0]}" ]
135
+ bt[1].keys.each do |key|
136
+ line << ":#{key} => '#{bt[1][key]}'"
137
+ end
138
+ belongs_to << line.join(', ')
139
+ end
140
+ end
141
+
142
+ # Build has_many
143
+ has_many = []
144
+ if has_many_array.size > 0
145
+ has_many_array.each do |hm|
146
+ line = [ "has_many :#{hm[0]}" ]
147
+ hm[1].keys.each do |key|
148
+ line << ":#{key} => '#{hm[1][key]}'"
149
+ end
150
+ has_many << line.join(', ')
151
+ end
152
+ end
153
+
154
+ self.class.module_eval <<-"end_eval"
155
+ class ::#{class_name} < ActiveLDAP::Base
156
+ ldap_mapping :dnattr => "#{attr}, :prefix => "#{prefix}", :classes => "#{classes}"
157
+ #{belongs_to.join("\n")}
158
+ #{has_many.join("\n")}
159
+ end
160
+ end_eval
161
+ end
162
+
163
+ # Connect and bind to LDAP creating a class variable for use by all ActiveLDAP
164
+ # objects.
165
+ #
166
+ # == +config+
167
+ # +config+ must be a hash that may contain any of the following fields:
168
+ # :user, :password_block, :logger, :host, :port, :base, :bind_format, :try_sasl, :allow_anonymous
169
+ # :user specifies the username to bind with.
170
+ # :bind_format specifies the string to substitute the username into on bind. e.g. uid=%s,ou=People,dc=example,dc=com. Overrides @@bind_format.
171
+ # :password_block specifies a Proc object that will yield a String to be used as the password when called.
172
+ # :logger specifies a preconfigured Log4r::Logger to be used for all logging
173
+ # :host overrides the configuration.rb @@host setting with the LDAP server hostname
174
+ # :port overrides the configuration.rb @@port setting for the LDAP server port
175
+ # :base overwrites Base.base - this affects EVERYTHING
176
+ # :try_sasl indicates that a SASL bind should be attempted when binding to the server (default: false)
177
+ # :allow_anonymous indicates that a true anonymous bind is allowed when trying to bind to the server (default: true)
178
+ def Base.connect(config={}) # :user, :password_block, :logger
179
+ # Process config
180
+ # Class options
181
+ ## These will be replace by configuration.rb defaults if defined
182
+ @@config = {}
183
+ @@config[:host] = config[:host] || @@host
184
+ @@config[:port] = config[:port] || @@port
185
+ if config[:base]
186
+ Base.class_eval <<-"end_eval"
187
+ def Base.base
188
+ '#{config[:base]}'
189
+ end
190
+ end_eval
191
+ end
192
+ @@config[:bind_format] = config[:bind_format] || @@bind_format
193
+
194
+ @@logger = config[:logger] || nil
195
+ # Setup default logger to console
196
+ if @@logger.nil?
197
+ @@logger = Log4r::Logger.new('activeldap')
198
+ @@logger.level = Log4r::OFF
199
+ Log4r::StderrOutputter.new 'console'
200
+ @@logger.add('console')
201
+ end
202
+
203
+ # Method options
204
+ user = nil
205
+ password_block = nil
206
+ @@config[:allow_anonymous] = true
207
+ @@config[:try_sasl] = false
208
+
209
+ @@config[:user] = config[:user] || user
210
+ @@config[:allow_anonymous] = config[:allow_anonymous] if config.has_key? :allow_anonymous
211
+ @@config[:try_sasl] = config[:try_sasl]
212
+ @@config[:password_block] = config[:password_block] if config.has_key? :password_block
213
+
214
+ # Setup bind credentials
215
+ @@config[:user] = ENV['USER'] unless @@config[:user]
216
+
217
+ # Connect to LDAP
218
+ begin
219
+ # SSL using START_TLS
220
+ @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], true)
221
+ rescue
222
+ @@logger.warn "Warning: Failed to connect using TLS!"
223
+ begin
224
+ @@logger.warn "Warning: Attempting SSL connection . . ."
225
+ @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false)
226
+ # HACK: Load the schema here because otherwise you can't tell if the
227
+ # HACK: SSLConn is a real SSL connection.
228
+ @@schema = @@conn.schema() if @@schema.nil?
229
+ rescue
230
+ @@logger.warn "Warning: Attempting unencrypted connection . . ."
231
+ @@conn = LDAP::Conn.new(@@config[:host], @@config[:port])
232
+ end
233
+ end
234
+ @@logger.debug "Connected to #{@@config[:host]}:#{@@config[:port]}."
235
+
236
+ # Enforce LDAPv3
237
+ @@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
238
+
239
+ # Authenticate
240
+ do_bind
241
+
242
+ # Load Schema (if not straight SSL...)
243
+ begin
244
+ @@schema = @@conn.schema() if @@schema.nil?
245
+ rescue => detail
246
+ raise ConnectionError, "#{detail.exception} - LDAP connection failure, or server does not support schema queries."
247
+ end
248
+
249
+ # Cleanly return
250
+ return true
251
+ end # Base.connect
252
+
253
+ # Base.close
254
+ # This method deletes the LDAP connection object.
255
+ # This does NOT reset any overridden values from a Base.connect call.
256
+ def Base.close
257
+ @@conn.unbind unless @@conn.nil?
258
+ @@conn = nil
259
+ # Make sure it is cleaned up
260
+ ObjectSpace.garbage_collect
261
+ end
262
+
263
+ # Return the LDAP connection object currently in use
264
+ def Base.connection
265
+ return @@conn
266
+ end
267
+
268
+ # Set the LDAP connection avoiding Base.connect or multiplexing connections
269
+ def Base.connection=(conn)
270
+ @@conn = conn
271
+ end
272
+
273
+ # Return the schema object
274
+ def Base.schema
275
+ @@schema
276
+ end
277
+
278
+ # search
279
+ #
280
+ # Wraps Ruby/LDAP connection.search to make it easier to search for specific
281
+ # data without cracking open Base.connection
282
+ def Base.search(config={})
283
+ unless Base.connection
284
+ if @@config
285
+ ActiveLDAP::Base.connect(@@config)
286
+ else
287
+ ActiveLDAP::Base.connect
288
+ end
289
+ end
290
+
291
+ config[:filter] = 'objectClass=*' unless config.has_key? :filter
292
+ config[:attrs] = [] unless config.has_key? :attrs
293
+ config[:scope] = LDAP::LDAP_SCOPE_SUBTREE unless config.has_key? :scope
294
+ config[:base] = base() unless config.has_key? :base
295
+
296
+ values = []
297
+ config[:attrs] = config[:attrs].to_a # just in case
298
+
299
+ begin
300
+ @@conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m|
301
+ res = {}
302
+ res['dn'] = [m.dn.dup] # For consistency with the below
303
+ m.attrs.each do |attr|
304
+ if config[:attrs].member? attr or config[:attrs].empty?
305
+ res[attr] = m.vals(attr).dup
306
+ end
307
+ end
308
+ values.push(res)
309
+ end
310
+ rescue RuntimeError => detail
311
+ @@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}"
312
+ # Do nothing on failure
313
+ end
314
+ return values
315
+ end
316
+
317
+ # find
318
+ #
319
+ # Finds the first match for value where |value| is the value of some
320
+ # |field|, or the wildcard match. This is only useful for derived classes.
321
+ # usage: Subclass.find(:attribute => "cn", :value => "some*val", :objects => true)
322
+ # Subclass.find('some*val')
323
+ #
324
+ def Base.find(config = {})
325
+ unless Base.connection
326
+ if @@config
327
+ ActiveLDAP::Base.connect(@@config)
328
+ else
329
+ ActiveLDAP::Base.connect
330
+ end
331
+ end
332
+
333
+ if self.class == Class
334
+ klass = self.ancestors[0].to_s.split(':').last
335
+ real_klass = self.ancestors[0]
336
+ else
337
+ klass = self.class.to_s.split(':').last
338
+ real_klass = self.class
339
+ end
340
+
341
+ # Allow a single string argument
342
+ attr = dnattr()
343
+ objects = false
344
+ val = config
345
+ # Or a hash
346
+ if config.respond_to?"has_key?"
347
+ attr = config[:attribute] || dnattr()
348
+ val = config[:value] || '*'
349
+ objects = config[:objects]
350
+ end
351
+
352
+ matches = []
353
+
354
+ begin
355
+ # Get some attributes
356
+ @@conn.search(base(), LDAP::LDAP_SCOPE_SUBTREE, "(#{attr}=#{val})") do |m|
357
+ # Extract the dnattr value
358
+ dnval = m.dn.split(/,/)[0].split(/=/)[1]
359
+
360
+ if objects
361
+ return eval("#{real_klass}.new(m)")
362
+ else
363
+ return dnval
364
+ end
365
+ end
366
+ rescue RuntimeError => detail
367
+ @@logger.debug "No matches for #{attr}=#{val}"
368
+ # Do nothing on failure
369
+ end
370
+ return nil
371
+ end
372
+ private_class_method :find
373
+
374
+
375
+ # find_all
376
+ #
377
+ # Finds all matches for value where |value| is the value of some
378
+ # |field|, or the wildcard match. This is only useful for derived classes.
379
+ def Base.find_all(config = {})
380
+ unless Base.connection
381
+ if @@config
382
+ ActiveLDAP::Base.connect(@@config)
383
+ else
384
+ ActiveLDAP::Base.connect
385
+ end
386
+ end
387
+
388
+ if self.class == Class
389
+ real_klass = self.ancestors[0]
390
+ else
391
+ real_klass = self.class
392
+ end
393
+
394
+ # Allow a single string argument
395
+ val = config
396
+ objects = false
397
+ # Or a hash
398
+ if config.respond_to?"has_key?"
399
+ attr = config[:attribute] || dnattr()
400
+ val = config[:value] || '*'
401
+ objects = config[:objects]
402
+ end
403
+
404
+ matches = []
405
+
406
+ begin
407
+ # Get some attributes
408
+ @@conn.search(base(), LDAP::LDAP_SCOPE_SUBTREE, "(#{attr}=#{val})") do |m|
409
+ # Extract the dnattr value
410
+ dnval = m.dn.split(/,/)[0].split(/=/)[1]
411
+
412
+ if objects
413
+ matches.push(eval("#{real_klass}.new(m)"))
414
+ else
415
+ matches.push(dnval)
416
+ end
417
+ end
418
+ rescue RuntimeError => detail
419
+ #p @@conn.err2string(@@conn.err)
420
+ @@logger.debug "No matches for #{attr}=#{val}"
421
+ # Do nothing on failure
422
+ end
423
+ return matches
424
+ end
425
+ private_class_method :find_all
426
+
427
+ # Base.base
428
+ #
429
+ # This method when included into Base provides
430
+ # an inheritable, overwritable configuration setting
431
+ #
432
+ # This should be a string with the base of the
433
+ # ldap server such as 'dc=example,dc=com', and
434
+ # it should be overwritten by including
435
+ # configuration.rb into this class.
436
+ # When subclassing, the specified prefix will be concatenated.
437
+ def Base.base
438
+ 'dc=example,dc=com'
439
+ end
440
+
441
+ # Base.dnattr
442
+ #
443
+ # This is a placeholder for the class method that will
444
+ # be overridden on calling ldap_mapping in a subclass.
445
+ # Using a class method allows for clean inheritance from
446
+ # classes that already have a ldap_mapping.
447
+ def Base.dnattr
448
+ ''
449
+ end
450
+
451
+ # Base.required_classes
452
+ #
453
+ # This method when included into Base provides
454
+ # an inheritable, overwritable configuration setting
455
+ #
456
+ # The value should be the minimum required objectClasses
457
+ # to make an object in the LDAP server, or an empty array [].
458
+ # This should be overwritten by configuration.rb.
459
+ # Note that subclassing does not cause concatenation of
460
+ # arrays to occurs.
461
+ def Base.required_classes
462
+ []
463
+ end
464
+
465
+
466
+
467
+ ### All instance methods, etc
468
+
469
+ # new
470
+ #
471
+ # Creates a new instance of Base initializing all class and all
472
+ # initialization. Defines local defaults. See examples If multiple values
473
+ # exist for dnattr, the first one put here will be authoritative
474
+ # TODO: Add # support for relative distinguished names
475
+ # val can be a dn attribute value, a full DN, or a LDAP::Entry. The use
476
+ # with a LDAP::Entry is primarily meant for internal use by find and
477
+ # find_all.
478
+ def initialize(val='')
479
+ # Try a default connection if none made explicitly
480
+ unless Base.connection
481
+ # Use @@config if it has been prepopulated and the conn is down.
482
+ if @@config
483
+ ActiveLDAP::Base.connect(@@config)
484
+ else
485
+ ActiveLDAP::Base.connect
486
+ end
487
+ end
488
+ if val.class == LDAP::Entry
489
+ # Call import, which is basically initialize
490
+ # without accessing LDAP.
491
+ @@logger.debug "initialize: val is a LDAP::Entry - running import."
492
+ import(val)
493
+ return
494
+ end
495
+ if val.class != String
496
+ raise TypeError, "Object key must be a String"
497
+ end
498
+
499
+ @data = {} # where the r/w entry data is stored
500
+ @data.default = []
501
+ @ldap_data = {} # original ldap entry data
502
+ @ldap_data.default = []
503
+ @attr_methods = {} # list of valid method calls for attributes used for dereferencing
504
+
505
+ # Break val apart if it is a dn
506
+ if val.match(/^#{dnattr()}=([^,=]+),#{base()}$/i)
507
+ val = $1
508
+ elsif val.match(/[=,]/)
509
+ @@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN."
510
+ val = ''
511
+ end
512
+
513
+ # Do a search - if it exists, pull all data and parse schema, if not, just set the hierarchical data
514
+ if val.empty?
515
+ @exists = false
516
+ # Setup what should eb authoritative
517
+ @dn = "#{dnattr()}=#{val},#{base()}"
518
+ send(:apply_objectclass, required_classes())
519
+ else # do a search then
520
+ # Search for the existing entry
521
+ begin
522
+ # Get some attributes
523
+ Base.connection.search("#{dnattr()}=#{val},#{base()}", LDAP::LDAP_SCOPE_SUBTREE, "objectClass=*") do |m|
524
+ # Save DN
525
+ @dn = m.dn
526
+ # Load up data into tmp
527
+ @@logger.debug("loading entry: #{@dn}")
528
+ m.attrs.each do |attr|
529
+ # Load with subtypes just like @data
530
+ @@logger.debug("calling make_subtypes for m.vals(attr).dup")
531
+ safe_attr, value = make_subtypes(attr, m.vals(attr).dup)
532
+ @@logger.debug("finished make_subtypes for #{attr}")
533
+ # Add subtype to any existing values
534
+ if @ldap_data.has_key? safe_attr
535
+ value.each do |v|
536
+ @ldap_data[safe_attr].push(v)
537
+ end
538
+ else
539
+ @ldap_data[safe_attr] = value
540
+ end
541
+ end
542
+ end
543
+ @exists = true
544
+ # Populate schema data
545
+ send(:apply_objectclass, @ldap_data['objectClass'])
546
+
547
+ # Populate real data now that we have the schema with aliases
548
+ @ldap_data.each do |pair|
549
+ send(:attribute_method=, pair[0], pair[1].dup)
550
+ end
551
+
552
+ rescue LDAP::ResultError
553
+ @exists = false
554
+ # Create what should be the authoritative DN
555
+ @dn = "#{dnattr()}=#{val},#{base()}"
556
+ send(:apply_objectclass, required_classes())
557
+
558
+ # Setup dn attribute (later rdn too!)
559
+ attr_sym = "#{dnattr()}=".to_sym
560
+ @@logger.debug("new: setting dnattr: #{dnattr()} = #{val}")
561
+ send(attr_sym, val)
562
+ end
563
+ end
564
+ end # initialize
565
+
566
+ # Hide new in Base
567
+ private_class_method :new
568
+
569
+ # attributes
570
+ #
571
+ # Return attribute methods so that a program can determine available
572
+ # attributes dynamically without schema awareness
573
+ def attributes
574
+ @@logger.debug("stub: attributes called")
575
+ send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc
576
+ return @attr_methods.keys
577
+ end
578
+
579
+ # exists?
580
+ #
581
+ # Return whether the entry exists in LDAP or not
582
+ def exists?
583
+ @@logger.debug("stub: exists? called")
584
+ return @exists
585
+ end
586
+
587
+ # dn
588
+ #
589
+ # Return the authoritative dn
590
+ def dn
591
+ @@logger.debug("stub: dn called")
592
+ return @dn.dup
593
+ end
594
+
595
+ # validate
596
+ #
597
+ # Basic validation:
598
+ # - Verify that every 'MUST' specified in the schema has a value defined
599
+ # - Enforcement of undefined attributes is handled in the objectClass= method
600
+ # Must call enforce_types() first before enforcement can be guaranteed
601
+ def validate
602
+ @@logger.debug("stub: validate called")
603
+ # Clean up attr values, etc
604
+ send(:enforce_types)
605
+
606
+ # Validate objectclass settings
607
+ @data['objectClass'].each do |klass|
608
+ unless klass.class == String
609
+ raise TypeError, "Value in objectClass array is not a String. (#{klass.class}:#{klass.inspect})"
610
+ end
611
+ unless Base.schema.names("objectClasses").member? klass
612
+ raise ObjectClassError, "objectClass '#{klass}' unknown to LDAP server."
613
+ end
614
+ end
615
+
616
+ # make sure this doesn't drop any of the required objectclasses
617
+ required_classes().each do |oc|
618
+ unless @data['objectClass'].member? oc.to_s
619
+ raise ObjectClassError, "'#{oc}' must be a defined objectClass for class '#{self.class}' as set in the ldap_mapping"
620
+ end
621
+ end
622
+
623
+ # Make sure all MUST attributes have a value
624
+ @data['objectClass'].each do |objc|
625
+ @must.each do |req_attr|
626
+ deref = @attr_methods[req_attr]
627
+ if @data[deref] == []
628
+ raise AttributeEmpty,
629
+ "objectClass '#{objc}' requires attribute '#{Base.schema.attribute_aliases(req_attr).join(', ')}'"
630
+ end
631
+ end
632
+ end
633
+ @@logger.debug("stub: validate finished")
634
+ end
635
+
636
+
637
+ # delete
638
+ #
639
+ # Delete this entry from LDAP
640
+ def delete
641
+ @@logger.debug("stub: delete called")
642
+ begin
643
+ @@conn.delete(@dn)
644
+ @exists = false
645
+ rescue LDAP::ResultError => detail
646
+ raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
647
+ end
648
+ end
649
+
650
+
651
+ # write
652
+ #
653
+ # Write and validate this object into LDAP
654
+ # either adding or replacing attributes
655
+ # TODO: Binary data support
656
+ # TODO: Relative DN support
657
+ def write
658
+ @@logger.debug("stub: write called")
659
+ # Validate against the objectClass requirements
660
+ validate
661
+
662
+ # Put all changes into one change entry to ensure
663
+ # automatic rollback upon failure.
664
+ entry = []
665
+
666
+
667
+ # Expand subtypes to real ldap_data entries
668
+ # We can't reuse @ldap_data because an exception would leave
669
+ # an object in an unknown state
670
+ @@logger.debug("#write: dup'ing @ldap_data")
671
+ ldap_data = @ldap_data.dup
672
+ @@logger.debug("#write: dup finished @ldap_data")
673
+ @@logger.debug("#write: expanding subtypes in @ldap_data")
674
+ ldap_data.keys.each do |key|
675
+ ldap_data[key].each do |value|
676
+ if value.class == Hash
677
+ suffix, real_value = extract_subtypes(value)
678
+ if ldap_data.has_key? key + suffix
679
+ ldap_data[key + suffix].push(real_value)
680
+ else
681
+ ldap_data[key + suffix] = real_value
682
+ end
683
+ ldap_data[key].delete(value)
684
+ end
685
+ end
686
+ end
687
+ @@logger.debug("#write: subtypes expanded for @ldap_data")
688
+
689
+ # Expand subtypes to real data entries, but leave @data alone
690
+ @@logger.debug("#write: dup'ing @data")
691
+ data = @data.dup
692
+ @@logger.debug("#write: finished dup'ing @data")
693
+ @@logger.debug("#write: expanding subtypes for @data")
694
+ data.keys.each do |key|
695
+ data[key].each do |value|
696
+ if value.class == Hash
697
+ suffix, real_value = extract_subtypes(value)
698
+ if data.has_key? key + suffix
699
+ data[key + suffix].push(real_value)
700
+ else
701
+ data[key + suffix] = real_value
702
+ end
703
+ data[key].delete(value)
704
+ end
705
+ end
706
+ end
707
+ @@logger.debug("#write: subtypes expanded for @data")
708
+
709
+
710
+ if @exists
711
+ # Cycle through all attrs to determine action
712
+ action = {}
713
+
714
+ replaceable = []
715
+ # Now that all the subtypes will be treated as unique attributes
716
+ # we can see what's changed and add anything that is brand-spankin'
717
+ # new.
718
+ @@logger.debug("#write: traversing ldap_data determining replaces and deletes")
719
+ ldap_data.each do |pair|
720
+ suffix = ''
721
+ binary = 0
722
+
723
+ name, *suffix_a = pair[0].split(/;/)
724
+ suffix = ';'+ suffix_a.join(';') if suffix_a.size > 0
725
+ name = @attr_methods[name]
726
+ name = pair[0].split(/;/)[0] if name.nil? # for objectClass, or removed vals
727
+ value = data[name+suffix]
728
+
729
+ # Detect subtypes and account for them
730
+ binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name
731
+
732
+ replaceable.push(name+suffix)
733
+ if pair[1] != value
734
+ # Create mod entries
735
+ if not value.empty?
736
+ # Ditched delete then replace because attribs with no equality match rules
737
+ # will fails
738
+ @@logger.debug("updating attribute of existing entry: #{name+suffix}: #{value.inspect}")
739
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value))
740
+ else
741
+ # Since some types do not have equality matching rules, delete doesn't work
742
+ # Replacing with nothing is equivalent.
743
+ @@logger.debug("removing attribute from existing entry: #{name+suffix}")
744
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, []))
745
+ end
746
+ end
747
+ end
748
+ @@logger.debug("#write: finished traversing ldap_data")
749
+ @@logger.debug("#write: traversing data determining adds")
750
+ data.each do |pair|
751
+ suffix = ''
752
+ binary = 0
753
+
754
+ name, *suffix_a = pair[0].split(/;/)
755
+ suffix = ';' + suffix_a.join(';') if suffix_a.size > 0
756
+ name = @attr_methods[name]
757
+ name = pair[0].split(/;/)[0] if name.nil? # for obj class or removed vals
758
+ value = pair[1]
759
+
760
+ if not replaceable.member? name+suffix
761
+ # Detect subtypes and account for them
762
+ binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name
763
+ @@logger.debug("adding attribute to existing entry: #{name+suffix}: #{value.inspect}")
764
+ # REPLACE will function like ADD, but doesn't hit EQUALITY problems
765
+ # TODO: Added equality(attr) to Schema2
766
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) unless value.empty?
767
+ end
768
+ end
769
+ @@logger.debug("#write: traversing data complete")
770
+ begin
771
+ @@logger.debug("#write: modifying #{@dn}")
772
+ @@conn.modify(@dn, entry)
773
+ @@logger.debug("#write: modify successful")
774
+ rescue => detail
775
+ raise WriteError, "Could not update LDAP entry: #{detail}"
776
+ end
777
+ else # add everything!
778
+ @@logger.debug("#write: adding all attribute value pairs")
779
+ @@logger.debug("#write: adding #{@attr_methods[dnattr()].inspect} = #{data[@attr_methods[dnattr()]].inspect}")
780
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, @attr_methods[dnattr()],
781
+ data[@attr_methods[dnattr()]]))
782
+ @@logger.debug("#write: adding objectClass = #{data[@attr_methods['objectClass']].inspect}")
783
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, 'objectClass',
784
+ data[@attr_methods['objectClass']]))
785
+ data.each do |pair|
786
+ if pair[1].size > 0 and pair[0] != 'objectClass' and pair[0] != @attr_methods[dnattr()]
787
+ # Detect subtypes and account for them
788
+ if Base.schema.binary? pair[0].split(/;/)[0]
789
+ binary = LDAP::LDAP_MOD_BVALUES
790
+ else
791
+ binary = 0
792
+ end
793
+ @@logger.debug("adding attribute to new entry: #{pair[0].inspect}: #{pair[1].inspect}")
794
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1]))
795
+ end
796
+ end
797
+ begin
798
+ @@logger.debug("#write: adding #{@dn}")
799
+ @@conn.add(@dn, entry)
800
+ @@logger.debug("#write: add successful")
801
+ @exists = true
802
+ rescue LDAP::ResultError => detail
803
+ raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}"
804
+ end
805
+ end
806
+ @@logger.debug("#write: resetting @ldap_data to a dup of @data")
807
+ @ldap_data = @data.dup
808
+ @@logger.debug("#write: @ldap_data reset complete")
809
+ @@logger.debug("stub: write exitted")
810
+ end
811
+
812
+
813
+ # method_missing
814
+ #
815
+ # If a given method matches an attribute or an attribute alias
816
+ # then call the appropriate method.
817
+ # TODO: Determine if it would be better to define each allowed method
818
+ # using class_eval instead of using method_missing. This would
819
+ # give tab completion in irb.
820
+ def method_missing(name, *args)
821
+ @@logger.debug("stub: called method_missing(#{name.inspect}, #{args.inspect})")
822
+
823
+ # dynamically update the available attributes without requiring an
824
+ # explicit call. The cache 'last_oc' saves a lot of cpu time.
825
+ if @data['objectClass'] != @last_oc
826
+ @@logger.debug("method_missing(#{name.inspect}, #{args.inspect}): updating apply_objectclass(#{@data['objectClass'].inspect})")
827
+ send(:apply_objectclass, @data['objectClass'])
828
+ end
829
+ key = name.to_s
830
+ case key
831
+ when /^(\S+)=$/
832
+ real_key = $1
833
+ @@logger.debug("method_missing: attr_methods has_key? #{real_key}")
834
+ if @attr_methods.has_key? real_key
835
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1
836
+ @@logger.debug("method_missing: calling :attribute_method=(#{real_key}, #{args[0]})")
837
+ return send(:attribute_method=, real_key, args[0])
838
+ end
839
+ else
840
+ @@logger.debug("method_missing: attr_methods has_key? #{key}")
841
+ if @attr_methods.has_key? key
842
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1
843
+ return attribute_method(key, *args)
844
+ end
845
+ end
846
+ raise NoMethodError, "undefined method `#{key}' for #{self}"
847
+ end
848
+
849
+ # Add available attributes to the methods
850
+ alias_method :__methods, :methods
851
+ def methods
852
+ return __methods + attributes()
853
+ end
854
+
855
+
856
+ private
857
+
858
+ # import(LDAP::Entry)
859
+ #
860
+ # Overwrites an existing entry (usually called by new)
861
+ # with the data given in the data given in LDAP::Entry.
862
+ #
863
+ def import(entry=nil)
864
+ @@logger.debug("stub: import called")
865
+ if entry.class != LDAP::Entry
866
+ raise TypeError, "argument must be a LDAP::Entry"
867
+ end
868
+
869
+ @data = {} # where the r/w entry data is stored
870
+ @data.default = []
871
+ @ldap_data = {} # original ldap entry data
872
+ @ldap_data.default = []
873
+ @attr_methods = {} # list of valid method calls for attributes used for dereferencing
874
+
875
+ # Get some attributes
876
+ @dn = entry.dn
877
+ entry.attrs.each do |attr|
878
+ # Load with subtypes just like @data
879
+ @@logger.debug("calling make_subtypes for entry.vals(attr).dup")
880
+ safe_attr, value = make_subtypes(attr, entry.vals(attr).dup)
881
+ @@logger.debug("finished make_subtypes for #{attr}")
882
+ # Add subtype to any existing values
883
+ if @ldap_data.has_key? safe_attr
884
+ value.each do |v|
885
+ @ldap_data[safe_attr].push(v)
886
+ end
887
+ else
888
+ @ldap_data[safe_attr] = value
889
+ end
890
+ end
891
+ # Assume if we are importing it that it exists
892
+ @exists = true
893
+ # Populate schema data
894
+ send(:apply_objectclass, @ldap_data['objectClass'])
895
+
896
+ # Populate real data now that we have the schema with aliases
897
+ @ldap_data.each do |pair|
898
+ send(:attribute_method=, pair[0], pair[1].dup)
899
+ end
900
+ end # import
901
+
902
+ # enforce_types
903
+ #
904
+ # enforce_types applies your changes without attempting to write to LDAP. This means that
905
+ # if you set userCertificate to somebinary value, it will wrap it up correctly.
906
+ def enforce_types
907
+ @@logger.debug("stub: enforce_types called")
908
+ send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc
909
+ # Enforce attribute value formatting
910
+ @data.keys.each do |key|
911
+ @data[key] = attribute_input_handler(key, @data[key])
912
+ end
913
+ @@logger.debug("stub: enforce_types done")
914
+ return true
915
+ end
916
+
917
+ # apply_objectclass
918
+ #
919
+ # objectClass= special case for updating appropriately
920
+ # This updates the objectClass entry in @data. It also
921
+ # updating all required and allowed attributes while
922
+ # removing defined attributes that are no longer valid
923
+ # given the new objectclasses.
924
+ def apply_objectclass(val)
925
+ @@logger.debug("stub: objectClass=(#{val.inspect}) called")
926
+ new_oc = val
927
+ new_oc = [val] if new_oc.class != Array
928
+ return new_oc if @last_oc == new_oc
929
+
930
+ # Store for caching purposes
931
+ @last_oc = new_oc.dup
932
+
933
+ # Set the actual objectClass data
934
+ define_attribute_methods('objectClass')
935
+ @data['objectClass'] = new_oc.uniq
936
+
937
+ # Build |data| from schema
938
+ # clear attr_method mapping first
939
+ @attr_methods = {}
940
+ @must = []
941
+ @may = []
942
+ new_oc.each do |objc|
943
+ # get all attributes for the class
944
+ attributes = Base.schema.class_attributes(objc.to_s)
945
+ @must += attributes[:must]
946
+ @may += attributes[:may]
947
+ end
948
+ @must.uniq!
949
+ @may.uniq!
950
+ (@must+@may).each do |attr|
951
+ # Update attr_method with appropriate
952
+ define_attribute_methods(attr)
953
+ end
954
+
955
+ # Delete all now innew_ocid attributes given the new objectClasses
956
+ @data.keys.each do |key|
957
+ # If it's not a proper aliased attribute, drop it
958
+ unless @attr_methods.has_key? key
959
+ @data.delete(key)
960
+ end
961
+ end
962
+ end
963
+
964
+
965
+
966
+ # Enforce typing:
967
+ # Hashes are for subtypes
968
+ # Arrays are for multiple entries
969
+ def attribute_input_handler(attr, value)
970
+ @@logger.debug("stub: called attribute_input_handler(#{attr.inspect}, #{value.inspect})")
971
+ if attr.nil?
972
+ raise RuntimeError, 'The first argument, attr, must not be nil. Please report this as a bug!'
973
+ end
974
+ binary = Base.schema.binary_required? attr
975
+ single = Base.schema.single_value? attr
976
+ case value.class.to_s
977
+ when 'Array'
978
+ if single and value.size > 1
979
+ raise TypeError, "Attribute #{attr} can only have a single value"
980
+ end
981
+ value.map! do |entry|
982
+ if entry.class != Hash
983
+ @@logger.debug("coercing value for #{attr} into a string because nested values exceeds a useful depth: #{entry.inspect} -> #{entry.to_s}")
984
+ entry = entry.to_s
985
+ end
986
+ entry = attribute_input_handler(attr, entry)[0]
987
+ end
988
+ when 'Hash'
989
+ if value.keys.size > 1
990
+ raise TypeError, "Hashes must have one key-value pair only."
991
+ end
992
+ unless value.keys[0].match(/^(lang-[a-z][a-z]*)|(binary)$/)
993
+ @@logger.warn("unknown subtype did not match lang-* or binary: #{value.keys[0]}")
994
+ end
995
+ # Contents MUST be a String or an Array
996
+ if value.keys[0] != 'binary' and binary
997
+ suffix, real_value = extract_subtypes(value)
998
+ value = make_subtypes(name + suffix + ';binary', real_value)
999
+ end
1000
+ value = [value]
1001
+ when 'String'
1002
+ if binary
1003
+ value = {'binary' => value}
1004
+ end
1005
+ return [value]
1006
+ else
1007
+ value = [value.to_s]
1008
+ end
1009
+ return value
1010
+ end
1011
+
1012
+ # make_subtypes
1013
+ #
1014
+ # Makes the Hashized value from the full attributename
1015
+ # e.g. userCertificate;binary => "some_bin"
1016
+ # becomes userCertificate => {"binary" => "some_bin"}
1017
+ def make_subtypes(attr, value)
1018
+ @@logger.debug("stub: called make_subtypes(#{attr.inspect}, #{value.inspect})")
1019
+ return [attr, value] unless attr.match(/;/)
1020
+
1021
+ ret_attr, *subtypes = attr.split(/;/)
1022
+ return [ret_attr, [make_subtypes_helper(subtypes, value)]]
1023
+ end
1024
+
1025
+ # make_subtypes_helper
1026
+ #
1027
+ # This is a recursive function for building
1028
+ # nested hashed from multi-subtyped values
1029
+ def make_subtypes_helper(subtypes, value)
1030
+ @@logger.debug("stub: called make_subtypes_helper(#{subtypes.inspect}, #{value.inspect})")
1031
+ return value if subtypes.size == 0
1032
+ return {subtypes[0] => make_subtypes_helper(subtypes[1..-1], value)}
1033
+ end
1034
+
1035
+ # extract_subtypes
1036
+ #
1037
+ # Extracts all of the subtypes from a given set of nested hashes
1038
+ # and returns the attribute suffix and the final true value
1039
+ def extract_subtypes(value)
1040
+ @@logger.debug("stub: called extract_subtypes(#{value.inspect})")
1041
+ subtype = ''
1042
+ ret_val = value
1043
+ if value.class == Hash
1044
+ subtype = ';' + value.keys[0]
1045
+ ret_val = value[value.keys[0]]
1046
+ subsubtype = ''
1047
+ if ret_val.class == Hash
1048
+ subsubtype, ret_val = extract_subtypes(ret_val)
1049
+ end
1050
+ subtype += subsubtype
1051
+ end
1052
+ ret_val = [ret_val] unless ret_val.class == Array
1053
+ return subtype, ret_val
1054
+ end
1055
+
1056
+
1057
+ # Wrapper all bind activity
1058
+ def Base.do_bind()
1059
+ bind_dn = @@config[:bind_format] % [@@config[:user]]
1060
+ if @@config[:password_block]
1061
+ password = @@config[:password_block].call
1062
+ @@config[:password_block] = Proc.new { password }
1063
+ end
1064
+
1065
+ # Rough bind loop:
1066
+ # Attempt 1: SASL if available
1067
+ # Attempt 2: SIMPLE with credentials if password block
1068
+ # Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
1069
+ auth = false
1070
+ auth = do_sasl_bind(bind_dn) if @@config[:try_sasl]
1071
+ auth = do_simple_bind(bind_dn) unless auth
1072
+ auth = do_anonymous_bind(bind_dn) if not auth and @@config[:allow_anonymous]
1073
+
1074
+ unless auth
1075
+ raise AuthenticationError, "All authentication mechanisms failed"
1076
+ end
1077
+ return auth
1078
+ end
1079
+
1080
+
1081
+ # Base.do_anonymous_bind
1082
+ #
1083
+ # Bind to LDAP with the given DN, but with no password. (anonymous!)
1084
+ def Base.do_anonymous_bind(bind_dn)
1085
+ @@logger.info "Attempting anonymous authentication"
1086
+ begin
1087
+ @@conn.bind()
1088
+ return true
1089
+ rescue
1090
+ @@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
1091
+ @@logger.warn "Warning: Anonymous authentication failed."
1092
+ return false
1093
+ end
1094
+ end
1095
+
1096
+ # Base.do_simple_bind
1097
+ #
1098
+ # Bind to LDAP with the given DN and password_block.call()
1099
+ def Base.do_simple_bind(bind_dn)
1100
+ return false unless @@config[:password_block].respond_to? :call
1101
+ begin
1102
+ @@conn.bind(bind_dn, @@config[:password_block].call())
1103
+ return true
1104
+ rescue
1105
+ @@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
1106
+ @@logger.warn "Warning: SIMPLE authentication failed."
1107
+ return false
1108
+ end
1109
+ end
1110
+
1111
+ # Base.do_sasl_bind
1112
+ #
1113
+ # Bind to LDAP with the given DN using any available SASL methods
1114
+ def Base.do_sasl_bind(bind_dn)
1115
+ # Get all SASL mechanisms
1116
+ mechanisms = @@conn.root_dse[0]['supportedSASLMechanisms']
1117
+ # Use GSSAPI if available
1118
+ # Currently only GSSAPI is supported with Ruby/LDAP from
1119
+ # http://caliban.com/files/redhat/RPMS/i386/ruby-ldap-0.8.2-4.i386.rpm
1120
+ # TODO: Investigate further SASL support
1121
+ if mechanisms.respond_to? :member? and mechanisms.member? 'GSSAPI'
1122
+ begin
1123
+ @@conn.sasl_bind(bind_dn, 'GSSAPI')
1124
+ return true
1125
+ rescue
1126
+ @@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
1127
+ @@logger.warn "Warning: SASL GSSAPI authentication failed."
1128
+ return false
1129
+ end
1130
+ end
1131
+ return false
1132
+ end
1133
+
1134
+ # base
1135
+ #
1136
+ # Returns the value of self.class.base
1137
+ # This is just syntactic sugar
1138
+ def base
1139
+ @@logger.debug("stub: called base")
1140
+ self.class.base
1141
+ end
1142
+
1143
+ # required_classes
1144
+ #
1145
+ # Returns the value of self.class.required_classes
1146
+ # This is just syntactic sugar
1147
+ def required_classes
1148
+ @@logger.debug("stub: called required_classes")
1149
+ self.class.required_classes
1150
+ end
1151
+
1152
+ # dnattr
1153
+ #
1154
+ # Returns the value of self.class.dnattr
1155
+ # This is just syntactic sugar
1156
+ def dnattr
1157
+ @@logger.debug("stub: called dnattr")
1158
+ self.class.dnattr
1159
+ end
1160
+
1161
+ # attribute_method
1162
+ #
1163
+ # Return the value of the attribute called by method_missing?
1164
+ def attribute_method(method, not_array = false)
1165
+ @@logger.debug("stub: called attribute_method(#{method.inspect}, #{not_array.inspect}")
1166
+ attr = @attr_methods[method]
1167
+
1168
+ # Return a copy of the stored data
1169
+ return array_of(@data[attr].dup, false) if not_array
1170
+ return @data[attr]
1171
+ end
1172
+
1173
+
1174
+ # attribute_method=
1175
+ #
1176
+ # Set the value of the attribute called by method_missing?
1177
+ def attribute_method=(method, value)
1178
+ @@logger.debug("stub: called attribute_method=(#{method.inspect}, #{value.inspect})")
1179
+ # Get the attr and clean up the input
1180
+ attr = @attr_methods[method]
1181
+ @@logger.debug("attribute_method=(#{method.inspect}, #{value.inspect}): method maps to #{attr}")
1182
+
1183
+ # Assign the value
1184
+ @data[attr] = value
1185
+
1186
+ # Return the passed in value
1187
+ @@logger.debug("stub: exitting attribute_method=")
1188
+ return @data[attr]
1189
+ end
1190
+
1191
+
1192
+ # define_attribute_methods
1193
+ #
1194
+ # Make a method entry for _every_ alias of a valid attribute and map it
1195
+ # onto the first attribute passed in.
1196
+ def define_attribute_methods(attr)
1197
+ @@logger.debug("stub: called define_attribute_methods(#{attr.inspect})")
1198
+ if @attr_methods.has_key? attr
1199
+ return
1200
+ end
1201
+ aliases = Base.schema.attribute_aliases(attr)
1202
+ aliases.each do |ali|
1203
+ @@logger.debug("associating #{ali} --> #{attr}")
1204
+ @attr_methods[ali] = attr
1205
+ end
1206
+ @@logger.debug("stub: leaving define_attribute_methods(#{attr.inspect})")
1207
+ end
1208
+
1209
+ # array_of
1210
+ #
1211
+ # Returns the array form of a value, or not an array if
1212
+ # false is passed in.
1213
+ def array_of(value, to_a = true)
1214
+ @@logger.debug("stub: called array_of(#{value.inspect}, #{to_a.inspect})")
1215
+ if to_a
1216
+ case value.class.to_s
1217
+ when 'Array'
1218
+ return value
1219
+ when 'Hash'
1220
+ return [value]
1221
+ else
1222
+ return [value.to_s]
1223
+ end
1224
+ else
1225
+ case value.class.to_s
1226
+ when 'Array'
1227
+ return nil if value.size == 0
1228
+ return value[0] if value.size == 1
1229
+ return value
1230
+ when 'Hash'
1231
+ return value
1232
+ else
1233
+ return value.to_s
1234
+ end
1235
+ end
1236
+ end
1237
+
1238
+ end # Base
1239
+
1240
+ end # ActiveLDAP
1241
+
1242
+
1243
+
1244
+