treequel 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/ChangeLog +354 -0
  2. data/LICENSE +27 -0
  3. data/README +66 -0
  4. data/Rakefile +345 -0
  5. data/Rakefile.local +43 -0
  6. data/bin/treeirb +14 -0
  7. data/bin/treequel +229 -0
  8. data/examples/company-directory.rb +112 -0
  9. data/examples/ldap-monitor.rb +143 -0
  10. data/examples/ldap-monitor/public/css/master.css +328 -0
  11. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  12. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  13. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  14. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  15. data/examples/ldap-monitor/public/images/plug.png +0 -0
  16. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  17. data/examples/ldap-monitor/public/images/tick.png +0 -0
  18. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  19. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  20. data/examples/ldap-monitor/views/backends.erb +41 -0
  21. data/examples/ldap-monitor/views/connections.erb +74 -0
  22. data/examples/ldap-monitor/views/databases.erb +39 -0
  23. data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
  24. data/examples/ldap-monitor/views/index.erb +14 -0
  25. data/examples/ldap-monitor/views/layout.erb +35 -0
  26. data/examples/ldap-monitor/views/listeners.erb +30 -0
  27. data/examples/ldap_state.rb +62 -0
  28. data/lib/treequel.rb +145 -0
  29. data/lib/treequel/branch.rb +589 -0
  30. data/lib/treequel/branchcollection.rb +204 -0
  31. data/lib/treequel/branchset.rb +360 -0
  32. data/lib/treequel/constants.rb +604 -0
  33. data/lib/treequel/directory.rb +541 -0
  34. data/lib/treequel/exceptions.rb +32 -0
  35. data/lib/treequel/filter.rb +704 -0
  36. data/lib/treequel/mixins.rb +325 -0
  37. data/lib/treequel/schema.rb +245 -0
  38. data/lib/treequel/schema/attributetype.rb +252 -0
  39. data/lib/treequel/schema/ldapsyntax.rb +96 -0
  40. data/lib/treequel/schema/matchingrule.rb +124 -0
  41. data/lib/treequel/schema/matchingruleuse.rb +124 -0
  42. data/lib/treequel/schema/objectclass.rb +289 -0
  43. data/lib/treequel/sequel_integration.rb +26 -0
  44. data/lib/treequel/utils.rb +169 -0
  45. data/rake/191_compat.rb +26 -0
  46. data/rake/dependencies.rb +76 -0
  47. data/rake/helpers.rb +434 -0
  48. data/rake/hg.rb +261 -0
  49. data/rake/manual.rb +782 -0
  50. data/rake/packaging.rb +135 -0
  51. data/rake/publishing.rb +318 -0
  52. data/rake/rdoc.rb +30 -0
  53. data/rake/style.rb +62 -0
  54. data/rake/svn.rb +668 -0
  55. data/rake/testing.rb +187 -0
  56. data/rake/verifytask.rb +64 -0
  57. data/rake/win32.rb +190 -0
  58. data/spec/lib/constants.rb +93 -0
  59. data/spec/lib/helpers.rb +100 -0
  60. data/spec/treequel/branch_spec.rb +569 -0
  61. data/spec/treequel/branchcollection_spec.rb +213 -0
  62. data/spec/treequel/branchset_spec.rb +376 -0
  63. data/spec/treequel/directory_spec.rb +487 -0
  64. data/spec/treequel/filter_spec.rb +482 -0
  65. data/spec/treequel/mixins_spec.rb +330 -0
  66. data/spec/treequel/schema/attributetype_spec.rb +237 -0
  67. data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
  68. data/spec/treequel/schema/matchingrule_spec.rb +158 -0
  69. data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
  70. data/spec/treequel/schema/objectclass_spec.rb +262 -0
  71. data/spec/treequel/schema_spec.rb +118 -0
  72. data/spec/treequel/utils_spec.rb +49 -0
  73. data/spec/treequel_spec.rb +179 -0
  74. metadata +169 -0
@@ -0,0 +1,39 @@
1
+ <h2>Databases</h2>
2
+
3
+ <p><%= subsystem[:description ]%></p>
4
+
5
+ <table id="database-list" class="horizontal">
6
+ <thead>
7
+ <tr>
8
+ <th></th>
9
+ <th class="even">Shadow?</th>
10
+ <th class="odd">URI</th>
11
+ <th class="even">See Also</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <% databases.each_with_index do |database, i| %>
16
+ <% rowclass = i.divmod(2).last.zero? ? "even" : "odd" %>
17
+ <tr class="<%= rowclass %>">
18
+ <th><%= database[:monitoredInfo] %></th>
19
+ <td class="even icon">
20
+ <% if database[:monitorIsShadow].first == 'TRUE' %>
21
+ <img src="/images/tick.png" width="16" height="16" alt="Yes" />
22
+ <% else %>
23
+ &nbsp;
24
+ <% end %>
25
+ </td>
26
+ <td class="odd list">
27
+ <% if database[:labeledURI] %>
28
+ <span class="uri"><%= database[:labeledURI].first.gsub(/[^\x20-\x7f]/, '') %></span>
29
+ <% end %>
30
+ </td>
31
+ <td class="even">
32
+ <% if database[:seeAlso] %><span class="dn"><%= database[:seeAlso] %></span><% end %>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ </tbody>
37
+ </table>
38
+
39
+
@@ -0,0 +1,14 @@
1
+ <h2>The <%= h subsystem[:cn] %> Subsystem</h2>
2
+
3
+ <p><%= subsystem[:description].join(' ') %></p>
4
+
5
+ <h3>Subsystem</h3>
6
+ <pre><%= subsystem.to_ldif %></pre>
7
+
8
+ <h3>Contents</h3>
9
+ <dl>
10
+ <% contents.each do |branch| %>
11
+ <dt><%= branch.dn %></dt>
12
+ <dd><pre><%= branch.to_ldif %></pre></dd>
13
+ <% end %>
14
+ </dl>
@@ -0,0 +1,14 @@
1
+ <h2>Monitor Datapoints</h2>
2
+
3
+ <p>This server is running: <code><%= server_info %></code>.</p>
4
+
5
+ <p>Monitor Subsystems</p>
6
+
7
+ <dl>
8
+ <% for subsystem in datapoints.sort_by {|b| b[:cn].first } %>
9
+ <dt><a href="/<%= subsystem[:cn].first.downcase %>"><%= h subsystem[:cn].first %></a></dt>
10
+ <dd><%= h subsystem[:description].join(" ") %></dd>
11
+ <% end %>
12
+ </dl>
13
+
14
+
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+
4
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5
+ <head>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
7
+ <title>LDAP Web Monitor</title>
8
+
9
+ <link rel="icon" type="image/png" href="/images/treequel-favicon.png" />
10
+
11
+ <link rel="stylesheet" href="/css/master.css" type="text/css" media="screen" title="master"
12
+ charset="utf-8">
13
+ </head>
14
+ <body>
15
+
16
+ <div id="content">
17
+ <h1>LDAP Web Monitor</h1>
18
+
19
+ <div id="homelink">
20
+ <a href="/">&#x2191; Top</a>
21
+ </div>
22
+
23
+ <%= yield %>
24
+
25
+ </div>
26
+
27
+ <div id="footer">
28
+ <p>
29
+ <span class="name">Treequel LDAP Web Monitor</span>
30
+ <span class="vcsrev">$rev$</span>
31
+ </p>
32
+ </div>
33
+
34
+ </body>
35
+ </html>
@@ -0,0 +1,30 @@
1
+ <h2>Listeners</h2>
2
+
3
+ <p><%= subsystem[:description ]%></p>
4
+
5
+ <table id="listener-list" class="horizontal">
6
+ <thead>
7
+ <tr>
8
+ <th></th>
9
+ <th class="odd">URI</th>
10
+ <th class="even">Socket</th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% listeners.each_with_index do |listener, i| %>
15
+ <% rowclass = i.divmod(2).last.zero? ? "even" : "odd" %>
16
+ <tr class="<%= rowclass %>">
17
+ <th><%= listener[:monitoredInfo] %></th>
18
+ <td class="even icon">
19
+ <span class="uri"><%= listener[:labeledURI].first.gsub(/[^\x20-\x7f]/, '') %></span>
20
+ </td>
21
+ <td class="odd list">
22
+ <span class="socket"><%=
23
+ listener[:monitorConnectionLocalAddress].first[/IP=(.*)/, 1] %></span>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+
30
+
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+
4
+ # Use the OpenLDAP monitoring interface (cn=Monitor) to poll a collection of LDAP
5
+ # servers for collection information. See
6
+ #
7
+ # http://www.openldap.org/doc/admin24/monitoringslapd.html
8
+ #
9
+ # for details on how to set your servers up with this interface.
10
+ #
11
+ # Original ruby-ldap version by Mahlon E. Smith.
12
+ # Ported to Treequel by Michael Granger
13
+
14
+ BEGIN {
15
+ require 'pathname'
16
+ basedir = Pathname( __FILE__ ).dirname.parent
17
+ libdir = basedir + 'lib'
18
+
19
+ $LOAD_PATH.unshift( libdir.to_s )
20
+ }
21
+
22
+ require 'rubygems'
23
+ require 'treequel'
24
+
25
+ BIND_DN = 'cn=admin,cn=Monitor'
26
+ BIND_PASS = 'XXX'
27
+
28
+ SERVER_LIST = %w{
29
+ ldap1.acme.com
30
+ ldap2.acme.com
31
+ ldap3.acme.com
32
+ }
33
+
34
+ Treequel::Branch.include_operational_attrs = true
35
+
36
+ total_connections = 0
37
+ total_operations = 0
38
+
39
+ SERVER_LIST.each do |server|
40
+ con = ops = 0
41
+ dir = Treequel.directory( :host => server, :base_dn => 'cn=Monitor' )
42
+
43
+ conns = dir.cn( :connections ).filter( :objectClass => :monitorConnection ).
44
+ select( :monitorConnectionNumber, :monitorConnectionOpsExecuting )
45
+
46
+ dir.bound_as( BIND_DN, BIND_PASS ) do
47
+ con = conns.all.length
48
+ ops = conns.map( :monitorConnectionOpsExecuting ).
49
+ collect {|connops| connops.first.to_i }.
50
+ inject {|sum,connops| sum + connops }
51
+
52
+ puts "LDAP server: %s\n\t%s\n\tServing %d operations across %d clients\n\n" % [
53
+ server, dir[:monitoredInfo], ops, con
54
+ ]
55
+ end
56
+
57
+ total_connections = total_connections + con
58
+ total_operations = total_operations + ops
59
+ end
60
+
61
+ puts "\n%d active operations across %d clients\n" % [ total_operations, total_connections ]
62
+
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'logger'
4
+ require 'uri'
5
+ require 'uri/ldap'
6
+
7
+
8
+ ### Add an LDAPS URI type if none exists (ruby pre 1.8.7)
9
+ unless URI.const_defined?( :LDAPS )
10
+ module URI
11
+ class LDAPS < LDAP
12
+ DEFAULT_PORT = 636
13
+ end
14
+ @@schemes['LDAPS'] = LDAPS
15
+ end
16
+ end
17
+
18
+
19
+ # A library for interacting with LDAP modelled after Sequel.
20
+ #
21
+ # == Authors
22
+ #
23
+ # * Michael Granger <ged@FaerieMUD.org>
24
+ # * Mahlon E. Smith <mahlon@martini.nu>
25
+ #
26
+ # :include: LICENSE
27
+ #
28
+ #--
29
+ #
30
+ # Please see the file LICENSE in the base directory for licensing details.
31
+ #
32
+ module Treequel
33
+
34
+ # Library version
35
+ VERSION = '1.0.0'
36
+
37
+ # VCS revision
38
+ REVISION = %q$rev: e352bc86498a $
39
+
40
+ # Load the logformatters and some other stuff first
41
+ require 'treequel/constants'
42
+ require 'treequel/utils'
43
+
44
+ include Treequel::Constants
45
+
46
+
47
+ ### Logging
48
+ @default_logger = Logger.new( $stderr )
49
+ @default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
50
+
51
+ @default_log_formatter = Treequel::LogFormatter.new( @default_logger )
52
+ @default_logger.formatter = @default_log_formatter
53
+
54
+ @logger = @default_logger
55
+
56
+
57
+ class << self
58
+ # The log formatter that will be used when the logging subsystem is reset
59
+ attr_accessor :default_log_formatter
60
+
61
+ # The logger that will be used when the logging subsystem is reset
62
+ attr_accessor :default_logger
63
+
64
+ # The logger that's currently in effect
65
+ attr_accessor :logger
66
+ alias_method :log, :logger
67
+ alias_method :log=, :logger=
68
+ end
69
+
70
+
71
+ ### Reset the global logger object to the default
72
+ def self::reset_logger
73
+ self.logger = self.default_logger
74
+ self.logger.level = Logger::WARN
75
+ self.logger.formatter = self.default_log_formatter
76
+ end
77
+
78
+
79
+ ### Returns +true+ if the global logger has not been set to something other than
80
+ ### the default one.
81
+ def self::using_default_logger?
82
+ return self.logger == self.default_logger
83
+ end
84
+
85
+
86
+ ### Return the library's version string
87
+ def self::version_string( include_buildnum=false )
88
+ vstring = "%s %s" % [ self.name, VERSION ]
89
+ vstring << " (build %s)" % [ REVISION ] if include_buildnum
90
+ return vstring
91
+ end
92
+
93
+ ### Create a Treequel::Directory object, either from a Hash of options or an LDAP URL.
94
+ def self::directory( *args )
95
+ options = {}
96
+
97
+ args.each do |arg|
98
+ case arg
99
+ when String, URI
100
+ options.merge!( self.make_options_from_uri(arg) )
101
+ when Hash
102
+ options.merge!( arg )
103
+ else
104
+ raise ArgumentError, "unknown directory option %p: expected URL or Hash"
105
+ end
106
+ end
107
+
108
+ return Treequel::Directory.new( options )
109
+ end
110
+
111
+
112
+ ### Make an options hash suitable for passing to Treequel::Directory.new from the
113
+ ### given +uri+.
114
+ def self::make_options_from_uri( uri )
115
+ uri = URI( uri ) unless uri.is_a?( URI )
116
+ raise ArgumentError, "not an LDAP URL: %p" % [ uri ] unless
117
+ uri.scheme =~ /ldaps?/
118
+ options = {}
119
+
120
+ if uri.port
121
+ options[:port] = uri.port
122
+ elsif uri.scheme == 'ldaps'
123
+ options[:port] = LDAP::LDAPS_PORT
124
+ end
125
+
126
+ options[:connect_type] = :ssl if uri.scheme == 'ldaps'
127
+
128
+ options[:host] = uri.host if uri.host
129
+ options[:base_dn] = uri.dn unless uri.dn.nil? || uri.dn.empty?
130
+ options[:bind_dn] = uri.user if uri.user
131
+ options[:pass] = uri.password if uri.password
132
+
133
+ return options
134
+ end
135
+
136
+ # Now load the rest of the library
137
+ require 'treequel/exceptions'
138
+ require 'treequel/directory'
139
+ require 'treequel/branch'
140
+ require 'treequel/branchset'
141
+ require 'treequel/filter'
142
+
143
+ end # module Treequel
144
+
145
+
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'forwardable'
4
+ require 'ldap'
5
+ require 'ldap/ldif'
6
+
7
+ require 'treequel'
8
+ require 'treequel/mixins'
9
+ require 'treequel/constants'
10
+ require 'treequel/branchset'
11
+ require 'treequel/branchcollection'
12
+
13
+
14
+ # The object in Treequel that wraps an entry. It knows how to construct other branches
15
+ # for the entries below itself, and how to search for those entries.
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::Branch
29
+ include Comparable,
30
+ Treequel::Loggable,
31
+ Treequel::Constants
32
+
33
+ extend Treequel::Delegation,
34
+ Treequel::AttributeDeclarations
35
+
36
+
37
+
38
+ #################################################################
39
+ ### C L A S S M E T H O D S
40
+ #################################################################
41
+
42
+ # Whether or not to include operational attributes when fetching the entry for branches.
43
+ class << self
44
+ extend Treequel::AttributeDeclarations
45
+ @include_operational_attrs = false
46
+ predicate_attr :include_operational_attrs
47
+ end
48
+
49
+
50
+ ### Create a new Treequel::Branch from the given +entry+ hash from the specified +directory+.
51
+ def self::new_from_entry( entry, directory )
52
+ return self.new( directory, entry['dn'].first, entry )
53
+ end
54
+
55
+
56
+ #################################################################
57
+ ### I N S T A N C E M E T H O D S
58
+ #################################################################
59
+
60
+ ### Create a new Treequel::Branch with the given +directory+, +rdn_attribute+, +rdn_value+, and
61
+ ### +base_dn+. If the optional +entry+ object is given, it will be used to fetch values from
62
+ ### the directory; if it isn't provided, it will be fetched from the +directory+ the first
63
+ ### time it is needed.
64
+ def initialize( directory, dn, entry=nil )
65
+ raise ArgumentError, "invalid DN" unless dn.match( Patterns::DISTINGUISHED_NAME )
66
+ raise ArgumentError, "can't cast a %s to an LDAP::Entry" % [entry.class.name] unless
67
+ entry.nil? || entry.is_a?( Hash )
68
+
69
+ @directory = directory
70
+ @dn = dn
71
+ @entry = entry
72
+
73
+ @include_operational_attrs = self.class.include_operational_attrs?
74
+
75
+ @values = {}
76
+ end
77
+
78
+
79
+ ######
80
+ public
81
+ ######
82
+
83
+ # Delegate some other methods to a new Branchset via the #branchset method
84
+ def_method_delegators :branchset, :filter, :scope, :select, :limit, :timeout, :order
85
+
86
+
87
+ # The directory the branch's entry lives in
88
+ attr_reader :directory
89
+
90
+ # The DN of the branch
91
+ attr_reader :dn
92
+ alias_method :to_s, :dn
93
+
94
+ # Whether or not to include operational attributes when fetching the Branch's entry
95
+ predicate_attr :include_operational_attrs
96
+
97
+
98
+ ### Change the DN the Branch uses to look up its entry.
99
+
100
+ def dn=( newdn )
101
+ self.clear_caches
102
+ @dn = newdn
103
+ end
104
+
105
+
106
+ ### Enable or disable fetching of operational attributes (RC4512, section 3.4).
107
+ def include_operational_attrs=( new_setting )
108
+ self.clear_caches
109
+ @include_operational_attrs = new_setting ? true : false
110
+ end
111
+
112
+
113
+ ### Return the attribute/s which make up this Branch's RDN.
114
+ def rdn_attributes
115
+ return make_rdn_hash( self.rdn )
116
+ end
117
+
118
+
119
+ ### Return the LDAP::Entry associated with the receiver, fetching it from the
120
+ ### directory if necessary. Returns +nil+ if the entry doesn't exist in the
121
+ ### directory.
122
+ def entry
123
+ unless @entry
124
+ if self.include_operational_attrs?
125
+ @entry = self.directory.get_extended_entry( self )
126
+ else
127
+ @entry = self.directory.get_entry( self )
128
+ end
129
+ end
130
+
131
+ return @entry
132
+ end
133
+
134
+
135
+ ### Returns <tt>true</tt> if there is an entry currently in the directory with the
136
+ ### branch's DN.
137
+ def exists?
138
+ return self.entry ? true : false
139
+ end
140
+
141
+
142
+ ### Return the RDN of the branch.
143
+ def rdn
144
+ return self.split_dn( 2 ).first
145
+ end
146
+
147
+
148
+ ### Return the receiver's DN as an Array of attribute=value pairs. If +limit+ is non-zero,
149
+ ### only the <code>limit-1</code> first pairs are split from the DN, and the remainder
150
+ ### will be returned as the last element.
151
+ def split_dn( limit=0 )
152
+ return self.dn.split( /\s*,\s*/, limit )
153
+ end
154
+
155
+
156
+ ### Return the LDAP URI for this branch
157
+ def uri
158
+ uri = self.directory.uri
159
+ uri.dn = self.dn
160
+ return uri
161
+ end
162
+
163
+
164
+ ### Return the DN of this entry's parent, or nil if it doesn't have one.
165
+ def parent_dn
166
+ return nil if self.dn == self.directory.base_dn
167
+ return self.split_dn( 2 ).last
168
+ end
169
+
170
+
171
+ ### Return the Branch's immediate parent node.
172
+ def parent
173
+ return self.class.new( self.directory, self.parent_dn )
174
+ end
175
+
176
+
177
+ ### Return the Branch's immediate children as Treeque::Branch objects.
178
+ def children
179
+ return self.directory.search( self, :one, '(objectClass=*)' )
180
+ end
181
+
182
+
183
+ ### Return a Treequel::Branchset that will use the receiver as its base.
184
+ def branchset
185
+ return Treequel::Branchset.new( self )
186
+ end
187
+
188
+
189
+ ### Return Treequel::Schema::ObjectClass instances for each of the receiver's
190
+ ### objectClass attributes. If any +additional_classes+ are given,
191
+ ### merge them with the current list of the current objectClasses for the lookup.
192
+ def object_classes( *additional_classes )
193
+ schema = self.directory.schema
194
+
195
+ object_classes = self[:objectClass] || []
196
+ object_classes |= additional_classes.collect {|str| str.to_sym }
197
+ object_classes << :top if object_classes.empty?
198
+
199
+ return object_classes.
200
+ collect {|oid| schema.object_classes[oid.to_sym] }.
201
+ uniq
202
+ end
203
+
204
+
205
+ ### Return Treequel::Schema::AttributeType instances for each of the receiver's
206
+ ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
207
+ ### include the MUST attributeTypes for them as well. This can be used to predict what
208
+ ### attributes would need to be present for the entry to be saved if it added the
209
+ ### +additional_object_classes+ to its own.
210
+ def must_attribute_types( *additional_object_classes )
211
+ types = []
212
+ oclasses = self.object_classes( *additional_object_classes )
213
+ self.log.debug "Gathering MUST attribute types for %d objectClasses" % [ oclasses.length ]
214
+ oclasses.each do |oc|
215
+ self.log.debug " adding %p from %p" % [ oc.must, oc ]
216
+ types |= oc.must
217
+ end
218
+
219
+ return types
220
+ end
221
+
222
+
223
+ ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
224
+ ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
225
+ ### include the OIDs of the MUST attributes for them as well. This can be used to predict
226
+ ### what attributes would need to be present for the entry to be saved if it added the
227
+ ### +additional_object_classes+ to its own.
228
+ def must_oids( *additional_object_classes )
229
+ return self.object_classes( *additional_object_classes ).
230
+ collect {|oc| oc.must_oids }.flatten.uniq.reject {|val| val == '' }
231
+ end
232
+
233
+
234
+ ### Return a Hash of the attributes required by the Branch's objectClasses. If
235
+ ### any +additional_object_classes+ are given, include the attributes that would be
236
+ ### necessary for the entry to be saved with them.
237
+ def must_attributes_hash( *additional_object_classes )
238
+ attrhash = {}
239
+
240
+ self.must_attribute_types( *additional_object_classes ).each do |attrtype|
241
+ self.log.debug " adding attrtype %p to the MUST attributes hash" % [ attrtype ]
242
+
243
+ if attrtype.name == :objectClass
244
+ attrhash[ :objectClass ] = [:top] | additional_object_classes
245
+ elsif attrtype.single?
246
+ attrhash[ attrtype.name ] = ''
247
+ else
248
+ attrhash[ attrtype.name ] = ['']
249
+ end
250
+ end
251
+
252
+ return attrhash
253
+ end
254
+
255
+
256
+ ### Return Treequel::Schema::AttributeType instances for each of the receiver's
257
+ ### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
258
+ ### include the MAY attributeTypes for them as well. This can be used to predict what
259
+ ### optional attributes could be added to the entry if the +additional_object_classes+
260
+ ### were added to it.
261
+ def may_attribute_types( *additional_object_classes )
262
+ return self.object_classes( *additional_object_classes ).
263
+ collect {|oc| oc.may }.flatten.uniq
264
+ end
265
+
266
+
267
+ ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
268
+ ### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
269
+ ### include the OIDs of the MAY attributes for them as well. This can be used to predict
270
+ ### what optional attributes could be added to the entry if the +additional_object_classes+
271
+ ### were added to it.
272
+ def may_oids( *additional_object_classes )
273
+ return self.object_classes( *additional_object_classes ).
274
+ collect {|oc| oc.may_oids }.flatten.uniq
275
+ end
276
+
277
+
278
+ ### Return a Hash of the optional attributes allowed by the Branch's objectClasses. If
279
+ ### any +additional_object_classes+ are given, include the attributes that would be
280
+ ### available for the entry if it had them.
281
+ def may_attributes_hash( *additional_object_classes )
282
+ entry = self.entry
283
+ attrhash = {}
284
+
285
+ self.may_attribute_types( *additional_object_classes ).each do |attrtype|
286
+ self.log.debug " adding attrtype %p to the MAY attributes hash" % [ attrtype ]
287
+
288
+ if attrtype.single?
289
+ attrhash[ attrtype.name ] = nil
290
+ else
291
+ attrhash[ attrtype.name ] = []
292
+ end
293
+ end
294
+
295
+ attrhash[ :objectClass ] |= additional_object_classes
296
+ return attrhash
297
+ end
298
+
299
+
300
+ ### Return Treequel::Schema::AttributeType instances for the set of all of the receiver's
301
+ ### MUST and MAY attributeTypes.
302
+ def valid_attribute_types
303
+ return self.must_attribute_types | self.may_attribute_types
304
+ end
305
+
306
+
307
+ ### Return a uniqified Array of OIDs (numeric OIDs as Strings, named OIDs as Symbols) for
308
+ ### the set of all of the receiver's MUST and MAY attributeTypes.
309
+ def valid_attribute_oids
310
+ return self.must_oids | self.may_oids
311
+ end
312
+
313
+
314
+ ### Return a Hash of all the attributes allowed by the Branch's objectClasses. If
315
+ ### any +additional_object_classes+ are given, include the attributes that would be
316
+ ### available for the entry if it had them.
317
+ def valid_attributes_hash( *additional_object_classes )
318
+ must = self.must_attributes_hash( *additional_object_classes )
319
+ may = self.may_attributes_hash( *additional_object_classes )
320
+
321
+ return may.merge( must )
322
+ end
323
+
324
+
325
+ ### Return +true+ if the specified +attrname+ is a valid attributeType given the
326
+ ### receiver's current objectClasses.
327
+ def valid_attribute?( attroid )
328
+ attroid = attroid.to_sym if attroid.is_a?( String ) &&
329
+ attroid !~ NUMERICOID
330
+
331
+ return self.valid_attribute_types.any? { |a| a.names.include?( attroid ) }
332
+ end
333
+
334
+
335
+ ### Returns a human-readable representation of the object suitable for
336
+ ### debugging.
337
+ def inspect
338
+ return "#<%s:0x%0x %s @ %s entry=%p>" % [
339
+ self.class.name,
340
+ self.object_id * 2,
341
+ self.dn,
342
+ self.directory,
343
+ @entry,
344
+ ]
345
+ end
346
+
347
+
348
+ ### Return the entry's DN as an RFC1781-style UFN (User-Friendly Name).
349
+ def to_ufn
350
+ return LDAP.dn2ufn( self.dn )
351
+ end
352
+
353
+
354
+ ### Return the Branch as an LDAP::LDIF::Entry.
355
+ def to_ldif
356
+ ldif = "dn: %s\n" % [ self.dn ]
357
+
358
+ entry = self.entry || self.valid_attributes_hash
359
+
360
+ entry.keys.reject {|k| k == 'dn' }.each do |attribute|
361
+ entry[ attribute ].each do |val|
362
+ # self.log.debug " creating LDIF fragment for %p=%p" % [ attribute, val ]
363
+ frag = LDAP::LDIF.to_ldif( attribute, [val.dup] )
364
+ # self.log.debug " LDIF fragment is: %p" % [ frag ]
365
+ ldif << frag
366
+ end
367
+ end
368
+
369
+ return LDAP::LDIF::Entry.new( ldif )
370
+ end
371
+
372
+
373
+ ### Fetch the value/s associated with the given +attrname+ from the underlying entry.
374
+ def []( attrname )
375
+ attrsym = attrname.to_sym
376
+
377
+ unless @values.key?( attrsym )
378
+ directory = self.directory
379
+ entry = self.entry or return nil
380
+ return nil unless (( value = entry[attrsym.to_s] ))
381
+
382
+ self.log.debug " value is not cached; checking its attributeType"
383
+ if attribute = directory.schema.attribute_types[ attrsym ]
384
+ self.log.debug " attribute exists; checking the entry for a value"
385
+
386
+ syntax_oid = attribute.syntax_oid
387
+
388
+ if attribute.single?
389
+ self.log.debug " attributeType is SINGLE; unwrapping the Array"
390
+ @values[ attrsym ] = directory.convert_syntax_value( syntax_oid, value.first )
391
+ else
392
+ self.log.debug " attributeType is not SINGLE; keeping the Array"
393
+ @values[ attrsym ] = value.collect do |raw|
394
+ directory.convert_syntax_value( syntax_oid, raw )
395
+ end
396
+ @values[ attrsym ].freeze if @values[ attrsym ].is_a?( Array )
397
+ end
398
+
399
+ else
400
+ self.log.info "no attributeType for %p" % [ attrsym ]
401
+ @values[ attrsym ] = value
402
+ @values[ attrsym ].freeze
403
+ end
404
+ else
405
+ self.log.debug " value is cached."
406
+ end
407
+
408
+ return @values[ attrsym ]
409
+ end
410
+
411
+
412
+ ### Set attribute +attrname+ to a new +value+.
413
+ def []=( attrname, value )
414
+ value = [ value ] unless value.is_a?( Array )
415
+ self.log.debug "Modifying %s to %p" % [ attrname, value ]
416
+ self.directory.modify( self, attrname.to_s => value )
417
+ @values.delete( attrname.to_sym )
418
+ self.entry[ attrname.to_s ] = value
419
+ end
420
+
421
+
422
+ ### Make the changes to the entry specified by the given +attributes+.
423
+ def merge( attributes )
424
+ self.directory.modify( self, attributes )
425
+ self.clear_caches
426
+
427
+ return true
428
+ end
429
+ alias_method :modify, :merge
430
+
431
+
432
+ ### Delete the entry associated with the branch from the directory.
433
+ def delete
434
+ self.directory.delete( self )
435
+ self.clear_caches
436
+
437
+ return true
438
+ end
439
+
440
+
441
+ ### Create the entry for this Branch with the specified +attributes+. The +attributes+ should,
442
+ ### at a minimum, contain the pair `:objectClass => :someStructuralObjectClass`.
443
+ def create( attributes={} )
444
+ self.directory.create( self, attributes )
445
+ return self
446
+ end
447
+
448
+
449
+ ### Copy the entry for this Branch to a new entry with the given +newdn+ and merge in the
450
+ ### specified +attributes+.
451
+ def copy( newdn, attributes={} )
452
+
453
+ # Fully-qualify RDNs
454
+ newdn = newdn + ',' + self.parent_dn unless newdn.index(',')
455
+
456
+ self.log.debug "Creating a copy of %p at %p" % [ self.dn, newdn ]
457
+ newbranch = self.class.new( self.directory, newdn )
458
+
459
+ attributes = self.entry.merge( attributes )
460
+
461
+ self.log.debug " merged attributes: %p" % [ attributes ]
462
+ self.directory.create( newbranch, attributes )
463
+
464
+ return newbranch
465
+ end
466
+
467
+
468
+ ### Move the entry associated with this branch to a new entry indicated by +rdn+. If
469
+ ### any +attributes+ are given, also replace the corresponding attributes on the new
470
+ ### entry with them.
471
+ def move( rdn, attributes={} )
472
+ self.log.debug "Asking the directory to move me to an entry called %p" % [ rdn ]
473
+ return self.directory.move( self, rdn, attributes )
474
+ end
475
+
476
+
477
+ ### Comparable interface: Returns -1 if other_branch is less than, 0 if other_branch is
478
+ ### equal to, and +1 if other_branch is greater than the receiving Branch.
479
+ def <=>( other_branch )
480
+ # Try the easy cases first
481
+ return nil unless other_branch.respond_to?( :dn ) &&
482
+ other_branch.respond_to?( :split_dn )
483
+ return 0 if other_branch.dn == self.dn
484
+
485
+ # Try comparing reversed attribute pairs
486
+ rval = nil
487
+ pairseq = self.split_dn.reverse.zip( other_branch.split_dn.reverse )
488
+ pairseq.each do |a,b|
489
+ comparison = (a <=> b)
490
+ return comparison if !comparison.nil? && comparison.nonzero?
491
+ end
492
+
493
+ # The branches are related, so directly comparing DN strings will work
494
+ return self.dn <=> other_branch.dn
495
+ end
496
+
497
+
498
+ ### Fetch a new Treequel::Branch object for the child of the receiver with the specified
499
+ ### +rdn+.
500
+ def get_child( rdn )
501
+ newdn = [ rdn, self.dn ].join( ',' )
502
+ return self.class.new( self.directory, newdn )
503
+ end
504
+
505
+
506
+ ### Addition operator: return a Treequel::BranchCollection that contains both the receiver
507
+ ### and +other_branch+.
508
+ def +( other_branch )
509
+ return Treequel::BranchCollection.new( self.branchset, other_branch.branchset )
510
+ end
511
+
512
+
513
+
514
+ #########
515
+ protected
516
+ #########
517
+
518
+ ### Proxy method: if the first argument matches a valid attribute in the directory's
519
+ ### schema, return a new Branch for the RDN made by using the first two arguments as
520
+ ### attribute and value, and the remaining hash as additional attributes.
521
+ ###
522
+ ### E.g.,
523
+ ### branch = Treequel::Branch.new( directory, 'ou=people,dc=acme,dc=com' )
524
+ ### branch.uid( :chester ).dn
525
+ ### # => 'uid=chester,ou=people,dc=acme,dc=com'
526
+ ### branch.uid( :chester, :employeeType => 'admin' ).dn
527
+ ### # => 'uid=chester+employeeType=admin,ou=people,dc=acme,dc=com'
528
+ def method_missing( attribute, value=nil, additional_attributes={} )
529
+ return super( attribute ) if value.nil?
530
+ valid_types = self.directory.schema.attribute_types
531
+
532
+ return super(attribute) unless
533
+ valid_types.key?( attribute ) &&
534
+ additional_attributes.keys.all? {|ex_attr| valid_types.key?(ex_attr) }
535
+
536
+ rdn = rdn_from_pair_and_hash( attribute, value, additional_attributes )
537
+
538
+ return self.get_child( rdn )
539
+ end
540
+
541
+
542
+ ### Clear any cached values when the structural state of the object changes.
543
+ def clear_caches
544
+ @entry = nil
545
+ @values.clear
546
+ end
547
+
548
+
549
+ #######
550
+ private
551
+ #######
552
+
553
+ ### Make an RDN string (RFC 4514) from the primary +attribute+ and +value+ pair plus any
554
+ ### +additional_attributes+ (for multivalue RDNs).
555
+ def rdn_from_pair_and_hash( attribute, value, additional_attributes={} )
556
+ additional_attributes.merge!( attribute => value )
557
+ return additional_attributes.sort_by {|k,v| k.to_s }.
558
+ collect {|pair| pair.join('=') }.
559
+ join('+')
560
+ end
561
+
562
+
563
+ ### Split the given +rdn+ into an Array of the iniital RDN attribute and value, and a
564
+ ### Hash containing any additional pairs.
565
+ def pair_and_hash_from_rdn( rdn )
566
+ initial, *trailing = rdn.split( '+' )
567
+ initial_pair = initial.split( /\s*=\s*/ )
568
+ trailing_pairs = trailing.inject({}) do |hash,pair|
569
+ k,v = pair.split( /\s*=\s*/ )
570
+ hash[ k ] = v
571
+ hash
572
+ end
573
+
574
+ return initial_pair + [ trailing_pairs ]
575
+ end
576
+
577
+
578
+ ### Given an +RDN+, return a Hash of the key/value pairs which make it up.
579
+ def make_rdn_hash( rdn )
580
+ return rdn.split( /\s*\+\s*/ ).inject({}) do |attributes, pair|
581
+ attrname, value = pair.split(/\s*=\s*/)
582
+ attributes[ attrname ] = [ value ]
583
+ attributes
584
+ end
585
+ end
586
+
587
+ end # class Treequel::Branch
588
+
589
+