treequel 1.0.1 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +176 -14
- data/LICENSE +1 -1
- data/Rakefile +61 -45
- data/Rakefile.local +20 -0
- data/bin/treequel +502 -269
- data/examples/ldap-rack-auth.rb +2 -0
- data/lib/treequel.rb +221 -18
- data/lib/treequel/branch.rb +410 -201
- data/lib/treequel/branchcollection.rb +25 -13
- data/lib/treequel/branchset.rb +42 -40
- data/lib/treequel/constants.rb +233 -3
- data/lib/treequel/control.rb +95 -0
- data/lib/treequel/controls/contentsync.rb +138 -0
- data/lib/treequel/controls/pagedresults.rb +162 -0
- data/lib/treequel/controls/sortedresults.rb +216 -0
- data/lib/treequel/directory.rb +212 -65
- data/lib/treequel/exceptions.rb +11 -12
- data/lib/treequel/filter.rb +1 -12
- data/lib/treequel/mixins.rb +83 -47
- data/lib/treequel/monkeypatches.rb +29 -0
- data/lib/treequel/schema.rb +23 -19
- data/lib/treequel/schema/attributetype.rb +33 -3
- data/lib/treequel/schema/ldapsyntax.rb +0 -11
- data/lib/treequel/schema/matchingrule.rb +0 -11
- data/lib/treequel/schema/matchingruleuse.rb +0 -11
- data/lib/treequel/schema/objectclass.rb +36 -10
- data/lib/treequel/schema/table.rb +159 -0
- data/lib/treequel/sequel_integration.rb +7 -7
- data/lib/treequel/utils.rb +4 -66
- data/rake/documentation.rb +89 -0
- data/rake/helpers.rb +375 -307
- data/rake/hg.rb +16 -2
- data/rake/manual.rb +11 -6
- data/rake/packaging.rb +20 -35
- data/rake/publishing.rb +22 -62
- data/spec/lib/constants.rb +20 -0
- data/spec/lib/control_behavior.rb +44 -0
- data/spec/lib/matchers.rb +51 -0
- data/spec/treequel/branch_spec.rb +88 -29
- data/spec/treequel/branchcollection_spec.rb +24 -1
- data/spec/treequel/branchset_spec.rb +123 -51
- data/spec/treequel/control_spec.rb +48 -0
- data/spec/treequel/controls/contentsync_spec.rb +38 -0
- data/spec/treequel/controls/pagedresults_spec.rb +138 -0
- data/spec/treequel/controls/sortedresults_spec.rb +171 -0
- data/spec/treequel/directory_spec.rb +186 -16
- data/spec/treequel/mixins_spec.rb +42 -3
- data/spec/treequel/schema/attributetype_spec.rb +22 -20
- data/spec/treequel/schema/objectclass_spec.rb +67 -46
- data/spec/treequel/schema/table_spec.rb +134 -0
- data/spec/treequel_spec.rb +277 -15
- metadata +89 -108
- data/bin/treequel.orig +0 -963
- data/examples/ldap-monitor.rb +0 -143
- data/examples/ldap-monitor/public/css/master.css +0 -328
- data/examples/ldap-monitor/public/images/card_small.png +0 -0
- data/examples/ldap-monitor/public/images/chain_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
- data/examples/ldap-monitor/public/images/plug.png +0 -0
- data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
- data/examples/ldap-monitor/public/images/tick.png +0 -0
- data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
- data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
- data/examples/ldap-monitor/views/backends.erb +0 -41
- data/examples/ldap-monitor/views/connections.erb +0 -74
- data/examples/ldap-monitor/views/databases.erb +0 -39
- data/examples/ldap-monitor/views/dump_subsystem.erb +0 -14
- data/examples/ldap-monitor/views/index.erb +0 -14
- data/examples/ldap-monitor/views/layout.erb +0 -35
- data/examples/ldap-monitor/views/listeners.erb +0 -30
- data/rake/rdoc.rb +0 -30
- data/rake/win32.rb +0 -190
data/examples/ldap-rack-auth.rb
CHANGED
data/lib/treequel.rb
CHANGED
@@ -1,12 +1,19 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'ldap'
|
4
|
+
require 'ldap/schema'
|
5
|
+
require 'ldap/control'
|
6
|
+
|
3
7
|
require 'logger'
|
8
|
+
require 'pathname'
|
9
|
+
|
4
10
|
require 'uri'
|
5
11
|
require 'uri/ldap'
|
6
12
|
|
7
13
|
|
8
14
|
### Add an LDAPS URI type if none exists (ruby pre 1.8.7)
|
9
15
|
unless URI.const_defined?( :LDAPS )
|
16
|
+
# @private
|
10
17
|
module URI
|
11
18
|
class LDAPS < LDAP
|
12
19
|
DEFAULT_PORT = 636
|
@@ -18,24 +25,52 @@ end
|
|
18
25
|
|
19
26
|
# A library for interacting with LDAP modelled after Sequel.
|
20
27
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
|
29
|
-
#
|
30
|
-
#
|
28
|
+
# @version 1.0.4
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# # Connect to the directory at the specified URL
|
32
|
+
# dir = Treequel.directory( 'ldap://ldap.company.com/dc=company,dc=com' )
|
33
|
+
#
|
34
|
+
# # Get a list of email addresses of every person in the directory (as
|
35
|
+
# # long as people are under ou=people)
|
36
|
+
# dir.ou( :people ).filter( :mail ).map( :mail ).flatten
|
37
|
+
#
|
38
|
+
# # Get a list of all IP addresses for all hosts in any ou=hosts group
|
39
|
+
# # in the whole directory:
|
40
|
+
# dir.filter( :ou => :hosts ).collection.filter( :ipHostNumber ).
|
41
|
+
# map( :ipHostNumber ).flatten
|
42
|
+
#
|
43
|
+
# # Get all people in the directory in the form of a hash of names
|
44
|
+
# # keyed by email addresses
|
45
|
+
# dir.ou( :people ).filter( :mail ).to_hash( :mail, :cn )
|
46
|
+
#
|
47
|
+
# @author Michael Granger <ged@FaerieMUD.org>
|
48
|
+
# @author Mahlon E. Smith <mahlon@martini.nu>
|
31
49
|
#
|
50
|
+
# @see LICENSE Please see the LICENSE file in the base directory for licensing
|
51
|
+
# details.
|
52
|
+
#
|
32
53
|
module Treequel
|
33
54
|
|
34
55
|
# Library version
|
35
|
-
VERSION = '1.0.
|
56
|
+
VERSION = '1.0.4'
|
36
57
|
|
37
58
|
# VCS revision
|
38
|
-
REVISION = %q$
|
59
|
+
REVISION = %q$Revision: c8534439a5bc $
|
60
|
+
|
61
|
+
# Common paths for ldap.conf
|
62
|
+
COMMON_LDAP_CONF_PATHS = %w[
|
63
|
+
./ldaprc
|
64
|
+
~/.ldaprc
|
65
|
+
~/ldaprc
|
66
|
+
/etc/ldap/ldap.conf
|
67
|
+
/etc/openldap/ldap.conf
|
68
|
+
/etc/ldap.conf
|
69
|
+
/usr/local/etc/openldap/ldap.conf
|
70
|
+
/usr/local/etc/ldap.conf
|
71
|
+
/opt/local/etc/openldap/ldap.conf
|
72
|
+
/opt/local/etc/ldap.conf
|
73
|
+
]
|
39
74
|
|
40
75
|
# Load the logformatters and some other stuff first
|
41
76
|
require 'treequel/constants'
|
@@ -55,13 +90,14 @@ module Treequel
|
|
55
90
|
|
56
91
|
|
57
92
|
class << self
|
58
|
-
#
|
93
|
+
# @return [Logger::Formatter] the log formatter that will be used when the logging
|
94
|
+
# subsystem is reset
|
59
95
|
attr_accessor :default_log_formatter
|
60
96
|
|
61
|
-
#
|
97
|
+
# @return [Logger] the logger that will be used when the logging subsystem is reset
|
62
98
|
attr_accessor :default_logger
|
63
99
|
|
64
|
-
#
|
100
|
+
# @return [Logger] the logger that's currently in effect
|
65
101
|
attr_accessor :logger
|
66
102
|
alias_method :log, :logger
|
67
103
|
alias_method :log=, :logger=
|
@@ -69,6 +105,7 @@ module Treequel
|
|
69
105
|
|
70
106
|
|
71
107
|
### Reset the global logger object to the default
|
108
|
+
### @return [void]
|
72
109
|
def self::reset_logger
|
73
110
|
self.logger = self.default_logger
|
74
111
|
self.logger.level = Logger::WARN
|
@@ -83,14 +120,30 @@ module Treequel
|
|
83
120
|
end
|
84
121
|
|
85
122
|
|
86
|
-
###
|
123
|
+
### Get the Treequel version.
|
124
|
+
### @return [String] the library's version
|
87
125
|
def self::version_string( include_buildnum=false )
|
88
126
|
vstring = "%s %s" % [ self.name, VERSION ]
|
89
|
-
vstring << " (build %s)" % [ REVISION ] if include_buildnum
|
127
|
+
vstring << " (build %s)" % [ REVISION[/: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
|
90
128
|
return vstring
|
91
129
|
end
|
92
130
|
|
131
|
+
|
93
132
|
### Create a Treequel::Directory object, either from a Hash of options or an LDAP URL.
|
133
|
+
### @overload directory( uri )
|
134
|
+
### Create a Treequel::Directory object from an LDAP URI.
|
135
|
+
### @param [URI, String] uri The URI of the directory to connect to, its base, the bind DN,
|
136
|
+
### etc.
|
137
|
+
### @overload directory( options )
|
138
|
+
### @param [Hash] options the connection options
|
139
|
+
### @option options [String] :host ('localhost') The LDAP host to connect to
|
140
|
+
### @option options [Fixnum] :port (LDAP::LDAP_PORT) The port number to connect to
|
141
|
+
### @option options [Symbol] :connect_type (:tls) The type of connection to establish; :tls,
|
142
|
+
### :ssl, or :plain.
|
143
|
+
### @option options [String] :base_dn The base DN of the directory.
|
144
|
+
### @option options [String] :bind_dn The DN of the user to bind as.
|
145
|
+
### @option options [String] :pass The password to use when binding.
|
146
|
+
### @return [Treequel::Directory] the configured Directory object
|
94
147
|
def self::directory( *args )
|
95
148
|
options = {}
|
96
149
|
|
@@ -101,7 +154,7 @@ module Treequel
|
|
101
154
|
when Hash
|
102
155
|
options.merge!( arg )
|
103
156
|
else
|
104
|
-
raise ArgumentError, "unknown directory option %p: expected URL or Hash"
|
157
|
+
raise ArgumentError, "unknown directory option %p: expected URL or Hash" % [ arg ]
|
105
158
|
end
|
106
159
|
end
|
107
160
|
|
@@ -109,20 +162,45 @@ module Treequel
|
|
109
162
|
end
|
110
163
|
|
111
164
|
|
165
|
+
### Read the configuration from the specified +configfile+ and/or values in ENV and return
|
166
|
+
### a {Treequel::Directory} for the resulting configuration. Supports OpenLDAP and nss-style
|
167
|
+
### configuration-file directives, and honors the various OpenLDAP environment variables;
|
168
|
+
### see ldap.conf(5) for details.
|
169
|
+
### @param [String] configfile the path to the configuration file to use
|
170
|
+
### @return [Treequel::Directory] the configured Directory object
|
171
|
+
def self::directory_from_config( configfile=nil )
|
172
|
+
configfile ||= self.find_configfile or
|
173
|
+
raise ArgumentError, "No configfile specified, and no defaults present."
|
174
|
+
|
175
|
+
# Read options from ENV and the config file
|
176
|
+
fileopts = self.read_opts_from_config( configfile )
|
177
|
+
envopts = self.read_opts_from_environment
|
178
|
+
|
179
|
+
# Now merge all the options together with env > file > default
|
180
|
+
options = Treequel::Directory::DEFAULT_OPTIONS.merge( fileopts.merge(envopts) )
|
181
|
+
|
182
|
+
return Treequel::Directory.new( options )
|
183
|
+
end
|
184
|
+
|
185
|
+
|
112
186
|
### Make an options hash suitable for passing to Treequel::Directory.new from the
|
113
187
|
### given +uri+.
|
188
|
+
### @param [URI, String] uri the URI to parse for options
|
189
|
+
### @return [Hash] the parsed options
|
114
190
|
def self::make_options_from_uri( uri )
|
115
191
|
uri = URI( uri ) unless uri.is_a?( URI )
|
116
192
|
raise ArgumentError, "not an LDAP URL: %p" % [ uri ] unless
|
117
193
|
uri.scheme =~ /ldaps?/
|
118
194
|
options = {}
|
119
195
|
|
196
|
+
# Use either the scheme or the port from the URI to set the port
|
120
197
|
if uri.port
|
121
198
|
options[:port] = uri.port
|
122
199
|
elsif uri.scheme == 'ldaps'
|
123
200
|
options[:port] = LDAP::LDAPS_PORT
|
124
201
|
end
|
125
202
|
|
203
|
+
# Set the connection type if the scheme dictates it
|
126
204
|
options[:connect_type] = :ssl if uri.scheme == 'ldaps'
|
127
205
|
|
128
206
|
options[:host] = uri.host if uri.host
|
@@ -133,12 +211,137 @@ module Treequel
|
|
133
211
|
return options
|
134
212
|
end
|
135
213
|
|
214
|
+
|
215
|
+
### Find a valid ldap.conf config file by first looking in the LDAPCONF and LDAPRC environment
|
216
|
+
### variables, then searching the list of default paths in Treequel::COMMON_LDAP_CONF_PATHS.
|
217
|
+
### @return [String] the first valid path, or nil if no valid configfile could be found
|
218
|
+
### @raise [RuntimeError] if LDAPCONF or LDAPRC contains an invalid or unreadable path
|
219
|
+
def self::find_configfile
|
220
|
+
# LDAPCONF may be set to the path of a configuration file. This path can
|
221
|
+
# be absolute or relative to the current working directory.
|
222
|
+
if configfile = ENV['LDAPCONF']
|
223
|
+
Treequel.log.info "Using LDAPCONF environment variable for path to ldap.conf"
|
224
|
+
configpath = Pathname( configfile ).expand_path
|
225
|
+
raise "Config file #{configfile}, specified in the LDAPCONF environment variable, " +
|
226
|
+
"does not exist or isn't readable." unless configpath.readable?
|
227
|
+
return configpath
|
228
|
+
|
229
|
+
# The LDAPRC, if defined, should be the basename of a file in the current working
|
230
|
+
# directory or in the user's home directory.
|
231
|
+
elsif rcname = ENV['LDAPRC']
|
232
|
+
Treequel.log.info "Using LDAPRC environment variable for path to ldap.conf"
|
233
|
+
rcpath = Pathname( rcname ).expand_path
|
234
|
+
return rcpath if rcpath.readable?
|
235
|
+
rcpath = Pathname( "~" ).expand_path + rcname
|
236
|
+
return rcpath if rcpath.readable?
|
237
|
+
|
238
|
+
raise "Config file '#{rcname}', specified in the LDAPRC environment variable, does not " +
|
239
|
+
"exist or isn't readable."
|
240
|
+
else
|
241
|
+
Treequel.log.info "Searching common paths for ldap.conf"
|
242
|
+
return COMMON_LDAP_CONF_PATHS.collect {|path| Pathname(path) }.
|
243
|
+
find {|path| path.readable? }
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
### Read the ldap.conf-style configuration from +configfile+ and return it as a Hash.
|
249
|
+
### @param [String] configfile The path to the config file to read.
|
250
|
+
### @return [Hash] The hash of configuration values read from the file, in a form suitable for
|
251
|
+
### passing to {Treequel::Directory#initialize}.
|
252
|
+
def self::read_opts_from_config( configfile )
|
253
|
+
Treequel.log.debug "Reading config options from %s..." % [ configfile ]
|
254
|
+
opts = {}
|
255
|
+
|
256
|
+
linecount = 0
|
257
|
+
IO.foreach( configfile ) do |line|
|
258
|
+
Treequel.log.debug " line: %p" % [ line ]
|
259
|
+
linecount += 1
|
260
|
+
case line
|
261
|
+
|
262
|
+
# URI <ldap[si]://[name[:port]] ...>
|
263
|
+
# :TODO: Support multiple URIs somehow?
|
264
|
+
when /^\s*URI\s+(\S+)/i
|
265
|
+
Treequel.log.debug " setting options from a URI: %p" % [ line ]
|
266
|
+
uriopts = self.make_options_from_uri( $1 )
|
267
|
+
opts.merge!( uriopts )
|
268
|
+
|
269
|
+
# BASE <base>
|
270
|
+
when /^\s*BASE\s+(\S+)/i
|
271
|
+
Treequel.log.debug " setting default base DN: %p" % [ line ]
|
272
|
+
opts[:base_dn] = $1
|
273
|
+
|
274
|
+
# BINDDN <dn>
|
275
|
+
when /^\s*BINDDN\s+(\S+)/i
|
276
|
+
Treequel.log.debug " setting bind DN: %p" % [ line ]
|
277
|
+
opts[:bind_dn] = $1
|
278
|
+
|
279
|
+
# bindpw <bindpw> (ldap_nss only)
|
280
|
+
when /^\s*bindpw\s+(\S+)/i
|
281
|
+
Treequel.log.debug " setting bind password from line %s" % [ linecount ]
|
282
|
+
opts[:pass] = $1
|
283
|
+
|
284
|
+
# HOST <name[:port] ...>
|
285
|
+
when /^\s*HOST\s+(\S+)/i
|
286
|
+
Treequel.log.debug " setting host: %p" % [ line ]
|
287
|
+
opts[:host] = $1
|
288
|
+
|
289
|
+
# PORT <port>
|
290
|
+
when /^\s*PORT\s+(\S+)/i
|
291
|
+
Treequel.log.debug " setting port: %p" % [ line ]
|
292
|
+
opts[:port] = $1.to_i
|
293
|
+
|
294
|
+
# SSL <on|off|start_tls>
|
295
|
+
when /^\s*SSL\s+(on|off|start_tls)/i
|
296
|
+
mode = $1.downcase
|
297
|
+
case mode
|
298
|
+
when 'on'
|
299
|
+
Treequel.log.debug " enabling plain SSL: %p" % [ line ]
|
300
|
+
opts[:port] = 636
|
301
|
+
opts[:connect_type] = :ssl
|
302
|
+
when 'off'
|
303
|
+
Treequel.log.debug " disabling SSL: %p" % [ line ]
|
304
|
+
opts[:port] = 389
|
305
|
+
opts[:connect_type] = :plain
|
306
|
+
when 'start_tls'
|
307
|
+
Treequel.log.debug " enabling TLS: %p" % [ line ]
|
308
|
+
opts[:port] = 389
|
309
|
+
opts[:connect_type] = :tls
|
310
|
+
else
|
311
|
+
Treequel.log.error "Unknown 'ssl' setting %p in %s line %d" %
|
312
|
+
[ mode, configfile, linecount ]
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
return opts
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
### Read OpenLDAP-style connection options from ENV and return them as a Hash.
|
323
|
+
### @return [Hash] The hash of configuration values read from the environment, in a form
|
324
|
+
### suitable for passing to {Treequel::Directory#initialize}.
|
325
|
+
def self::read_opts_from_environment
|
326
|
+
opts = {}
|
327
|
+
|
328
|
+
opts.merge!( self.make_options_from_uri(ENV['LDAPURI']) ) if ENV['LDAPURI']
|
329
|
+
opts[:host] = ENV['LDAPHOST'] if ENV['LDAPHOST']
|
330
|
+
opts[:port] = ENV['LDAPPORT'].to_i if ENV['LDAPPORT']
|
331
|
+
opts[:bind_dn] = ENV['LDAPBINDDN'] if ENV['LDAPBINDDN']
|
332
|
+
opts[:base_dn] = ENV['LDAPBASE'] if ENV['LDAPBASE']
|
333
|
+
|
334
|
+
return opts
|
335
|
+
end
|
336
|
+
|
337
|
+
|
136
338
|
# Now load the rest of the library
|
137
339
|
require 'treequel/exceptions'
|
138
340
|
require 'treequel/directory'
|
139
341
|
require 'treequel/branch'
|
140
342
|
require 'treequel/branchset'
|
141
343
|
require 'treequel/filter'
|
344
|
+
require 'treequel/monkeypatches'
|
142
345
|
|
143
346
|
end # module Treequel
|
144
347
|
|
data/lib/treequel/branch.rb
CHANGED
@@ -13,41 +13,44 @@ require 'treequel/branchcollection'
|
|
13
13
|
|
14
14
|
# The object in Treequel that wraps an entry. It knows how to construct other branches
|
15
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
16
|
class Treequel::Branch
|
29
17
|
include Comparable,
|
30
18
|
Treequel::Loggable,
|
31
|
-
Treequel::Constants
|
19
|
+
Treequel::Constants,
|
20
|
+
Treequel::Constants::Patterns
|
32
21
|
|
33
22
|
extend Treequel::Delegation,
|
34
23
|
Treequel::AttributeDeclarations
|
35
24
|
|
36
25
|
|
26
|
+
# The default width of LDIF output
|
27
|
+
DEFAULT_LDIF_WIDTH = 70
|
28
|
+
|
29
|
+
# The characters to use to fold an LDIF line (newline + a space)
|
30
|
+
LDIF_FOLD_SEPARATOR = "\n "
|
31
|
+
|
37
32
|
|
38
33
|
#################################################################
|
39
34
|
### C L A S S M E T H O D S
|
40
35
|
#################################################################
|
41
36
|
|
42
|
-
# Whether or not to include operational attributes
|
37
|
+
# [Boolean] Whether or not to include operational attributes by default.
|
38
|
+
@include_operational_attrs = false
|
39
|
+
|
40
|
+
# Whether or not to include operational attributes when fetching the
|
41
|
+
# entry for branches.
|
43
42
|
class << self
|
44
43
|
extend Treequel::AttributeDeclarations
|
45
|
-
@include_operational_attrs = false
|
46
44
|
predicate_attr :include_operational_attrs
|
47
45
|
end
|
48
46
|
|
49
47
|
|
50
48
|
### Create a new Treequel::Branch from the given +entry+ hash from the specified +directory+.
|
49
|
+
###
|
50
|
+
### @param [LDAP::Entry] entry The raw entry object the Branch is wrapping.
|
51
|
+
### @param [Treequel::Directory] directory The directory object the Branch is from.
|
52
|
+
###
|
53
|
+
### @return [Treequel::Branch] The new branch object.
|
51
54
|
def self::new_from_entry( entry, directory )
|
52
55
|
return self.new( directory, entry['dn'].first, entry )
|
53
56
|
end
|
@@ -57,10 +60,14 @@ class Treequel::Branch
|
|
57
60
|
### I N S T A N C E M E T H O D S
|
58
61
|
#################################################################
|
59
62
|
|
60
|
-
### Create a new Treequel::Branch with the given +directory+, +
|
61
|
-
###
|
62
|
-
###
|
63
|
+
### Create a new Treequel::Branch with the given +directory+, +dn+, and an optional +entry+.
|
64
|
+
### If the optional +entry+ object is given, it will be used to fetch values from the
|
65
|
+
### directory; if it isn't provided, it will be fetched from the +directory+ the first
|
63
66
|
### time it is needed.
|
67
|
+
###
|
68
|
+
### @param [Treequel::Directory] directory The directory the Branch belongs to.
|
69
|
+
### @param [String] dn The DN of the entry the Branch is wrapping.
|
70
|
+
### @param [LDAP::Entry, Hash] entry The entry object if it's already been fetched.
|
64
71
|
def initialize( directory, dn, entry=nil )
|
65
72
|
raise ArgumentError, "invalid DN" unless dn.match( Patterns::DISTINGUISHED_NAME )
|
66
73
|
raise ArgumentError, "can't cast a %s to an LDAP::Entry" % [entry.class.name] unless
|
@@ -69,10 +76,9 @@ class Treequel::Branch
|
|
69
76
|
@directory = directory
|
70
77
|
@dn = dn
|
71
78
|
@entry = entry
|
79
|
+
@values = {}
|
72
80
|
|
73
81
|
@include_operational_attrs = self.class.include_operational_attrs?
|
74
|
-
|
75
|
-
@values = {}
|
76
82
|
end
|
77
83
|
|
78
84
|
|
@@ -83,11 +89,16 @@ class Treequel::Branch
|
|
83
89
|
# Delegate some other methods to a new Branchset via the #branchset method
|
84
90
|
def_method_delegators :branchset, :filter, :scope, :select, :limit, :timeout, :order
|
85
91
|
|
92
|
+
# Delegate some methods to the Branch's directory via its accessor
|
93
|
+
def_method_delegators :directory, :controls, :referrals
|
94
|
+
|
86
95
|
|
87
96
|
# The directory the branch's entry lives in
|
97
|
+
# @return [Treequel::Directory]
|
88
98
|
attr_reader :directory
|
89
99
|
|
90
|
-
# The DN of the branch
|
100
|
+
# The DN of the branch.
|
101
|
+
# @return [String]
|
91
102
|
attr_reader :dn
|
92
103
|
alias_method :to_s, :dn
|
93
104
|
|
@@ -96,7 +107,9 @@ class Treequel::Branch
|
|
96
107
|
|
97
108
|
|
98
109
|
### Change the DN the Branch uses to look up its entry.
|
99
|
-
|
110
|
+
###
|
111
|
+
### @param [String] newdn The new DN.
|
112
|
+
### @return [void]
|
100
113
|
def dn=( newdn )
|
101
114
|
self.clear_caches
|
102
115
|
@dn = newdn
|
@@ -104,6 +117,9 @@ class Treequel::Branch
|
|
104
117
|
|
105
118
|
|
106
119
|
### Enable or disable fetching of operational attributes (RC4512, section 3.4).
|
120
|
+
###
|
121
|
+
### @param [Boolean] new_setting
|
122
|
+
### @return [void]
|
107
123
|
def include_operational_attrs=( new_setting )
|
108
124
|
self.clear_caches
|
109
125
|
@include_operational_attrs = new_setting ? true : false
|
@@ -111,6 +127,7 @@ class Treequel::Branch
|
|
111
127
|
|
112
128
|
|
113
129
|
### Return the attribute/s which make up this Branch's RDN.
|
130
|
+
### @return [Hash<Symbol => String>] The Branch's RDN attributes as a Hash.
|
114
131
|
def rdn_attributes
|
115
132
|
return make_rdn_hash( self.rdn )
|
116
133
|
end
|
@@ -119,41 +136,40 @@ class Treequel::Branch
|
|
119
136
|
### Return the LDAP::Entry associated with the receiver, fetching it from the
|
120
137
|
### directory if necessary. Returns +nil+ if the entry doesn't exist in the
|
121
138
|
### directory.
|
139
|
+
###
|
140
|
+
### @return [LDAP::Entry] The entry wrapped by the Branch.
|
122
141
|
def entry
|
123
|
-
|
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
|
142
|
+
@entry ||= self.lookup_entry
|
132
143
|
end
|
133
144
|
|
134
145
|
|
135
146
|
### Returns <tt>true</tt> if there is an entry currently in the directory with the
|
136
147
|
### branch's DN.
|
148
|
+
### @return [Boolean]
|
137
149
|
def exists?
|
138
150
|
return self.entry ? true : false
|
139
151
|
end
|
140
152
|
|
141
153
|
|
142
154
|
### Return the RDN of the branch.
|
155
|
+
### @return [String]
|
143
156
|
def rdn
|
144
157
|
return self.split_dn( 2 ).first
|
145
158
|
end
|
146
159
|
|
147
160
|
|
148
|
-
### Return the receiver's DN as an Array of attribute=value pairs.
|
149
|
-
###
|
150
|
-
###
|
161
|
+
### Return the receiver's DN as an Array of attribute=value pairs.
|
162
|
+
###
|
163
|
+
### @param [Fixnum] limit If non-zero, only the <code>limit-1</code> first pairs
|
164
|
+
### are split from the DN, and the remainder will be returned as the last
|
165
|
+
### element.
|
151
166
|
def split_dn( limit=0 )
|
152
167
|
return self.dn.split( /\s*,\s*/, limit )
|
153
168
|
end
|
154
169
|
|
155
170
|
|
156
171
|
### Return the LDAP URI for this branch
|
172
|
+
### @return [URI]
|
157
173
|
def uri
|
158
174
|
uri = self.directory.uri
|
159
175
|
uri.dn = self.dn
|
@@ -162,6 +178,7 @@ class Treequel::Branch
|
|
162
178
|
|
163
179
|
|
164
180
|
### Return the DN of this entry's parent, or nil if it doesn't have one.
|
181
|
+
### @return [String]
|
165
182
|
def parent_dn
|
166
183
|
return nil if self.dn == self.directory.base_dn
|
167
184
|
return self.split_dn( 2 ).last
|
@@ -169,171 +186,44 @@ class Treequel::Branch
|
|
169
186
|
|
170
187
|
|
171
188
|
### Return the Branch's immediate parent node.
|
189
|
+
### @return [Treequel::Branch]
|
172
190
|
def parent
|
173
191
|
return self.class.new( self.directory, self.parent_dn )
|
174
192
|
end
|
175
193
|
|
176
194
|
|
195
|
+
### Perform a search with the specified +scope+, +filter+, and +parameters+
|
196
|
+
### using the receiver as the base.
|
197
|
+
###
|
198
|
+
### @param scope (see Trequel::Directory#search)
|
199
|
+
### @param filter (see Trequel::Directory#search)
|
200
|
+
### @param parameters (see Trequel::Directory#search)
|
201
|
+
### @param block (see Trequel::Directory#search)
|
202
|
+
###
|
203
|
+
### @return [Array<Treequel::Branch>] the search results
|
204
|
+
def search( scope=:subtree, filter='(objectClass=*)', parameters={}, &block )
|
205
|
+
return self.directory.search( self, scope, filter, parameters, &block )
|
206
|
+
end
|
207
|
+
|
208
|
+
|
177
209
|
### Return the Branch's immediate children as Treeque::Branch objects.
|
210
|
+
### @return [Array<Treequel::Branch>]
|
178
211
|
def children
|
179
|
-
return self.
|
212
|
+
return self.search( :one, '(objectClass=*)' )
|
180
213
|
end
|
181
214
|
|
182
215
|
|
183
216
|
### Return a Treequel::Branchset that will use the receiver as its base.
|
217
|
+
### @return [Treequel::Branchset]
|
184
218
|
def branchset
|
185
219
|
return Treequel::Branchset.new( self )
|
186
220
|
end
|
187
221
|
|
188
222
|
|
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
223
|
### Returns a human-readable representation of the object suitable for
|
336
224
|
### debugging.
|
225
|
+
###
|
226
|
+
### @return [String]
|
337
227
|
def inspect
|
338
228
|
return "#<%s:0x%0x %s @ %s entry=%p>" % [
|
339
229
|
self.class.name,
|
@@ -346,23 +236,23 @@ class Treequel::Branch
|
|
346
236
|
|
347
237
|
|
348
238
|
### Return the entry's DN as an RFC1781-style UFN (User-Friendly Name).
|
239
|
+
### @return [String]
|
349
240
|
def to_ufn
|
350
241
|
return LDAP.dn2ufn( self.dn )
|
351
242
|
end
|
352
243
|
|
353
244
|
|
354
245
|
### Return the Branch as an LDAP::LDIF::Entry.
|
355
|
-
|
246
|
+
### @return [String]
|
247
|
+
def to_ldif( width=DEFAULT_LDIF_WIDTH )
|
356
248
|
ldif = "dn: %s\n" % [ self.dn ]
|
357
249
|
|
358
250
|
entry = self.entry || self.valid_attributes_hash
|
251
|
+
self.log.debug " making LDIF from an entry: %p" % [ entry ]
|
359
252
|
|
360
253
|
entry.keys.reject {|k| k == 'dn' }.each do |attribute|
|
361
254
|
entry[ attribute ].each do |val|
|
362
|
-
|
363
|
-
frag = LDAP::LDIF.to_ldif( attribute, [val.dup] )
|
364
|
-
# self.log.debug " LDIF fragment is: %p" % [ frag ]
|
365
|
-
ldif << frag
|
255
|
+
ldif << ldif_for_attr( attribute, val, width )
|
366
256
|
end
|
367
257
|
end
|
368
258
|
|
@@ -370,7 +260,40 @@ class Treequel::Branch
|
|
370
260
|
end
|
371
261
|
|
372
262
|
|
263
|
+
### Make LDIF for the given +attribute+ and its +values+, wrapping at the given
|
264
|
+
### +width+.
|
265
|
+
###
|
266
|
+
### @param [String] attribute the attribute
|
267
|
+
### @param [Array<String>] values the values for the given +attribute+
|
268
|
+
### @param [Fixnum] width the maximum width of the lines to return
|
269
|
+
def ldif_for_attr( attribute, values, width )
|
270
|
+
ldif = ''
|
271
|
+
|
272
|
+
Array( values ).each do |val|
|
273
|
+
line = "#{attribute}:"
|
274
|
+
|
275
|
+
if val =~ /^#{LDIF_SAFE_STRING}$/
|
276
|
+
line << ' ' << val.to_s
|
277
|
+
else
|
278
|
+
line << ': ' << [ val ].pack( 'm' ).chomp
|
279
|
+
end
|
280
|
+
|
281
|
+
# calculate how many times the line needs to be split, then add any
|
282
|
+
# additional splits that need to be added because of the additional
|
283
|
+
# fold characters
|
284
|
+
splits = ( line.length / width )
|
285
|
+
splits += ( splits * LDIF_FOLD_SEPARATOR.length ) / width
|
286
|
+
splits.times {|i| line[ width * (i+1), 0 ] = LDIF_FOLD_SEPARATOR }
|
287
|
+
|
288
|
+
ldif << line << "\n"
|
289
|
+
end
|
290
|
+
|
291
|
+
return ldif
|
292
|
+
end
|
293
|
+
|
294
|
+
|
373
295
|
### Fetch the value/s associated with the given +attrname+ from the underlying entry.
|
296
|
+
### @return [Array, String]
|
374
297
|
def []( attrname )
|
375
298
|
attrsym = attrname.to_sym
|
376
299
|
|
@@ -409,7 +332,21 @@ class Treequel::Branch
|
|
409
332
|
end
|
410
333
|
|
411
334
|
|
335
|
+
### Fetch one or more values from the entry.
|
336
|
+
###
|
337
|
+
### @param [Array<Symbol, String>] attributes The attributes to fetch values for.
|
338
|
+
### @return [Array<String>] The values which correspond to +attributes+.
|
339
|
+
def values_at( *attributes )
|
340
|
+
return attributes.collect do |attribute|
|
341
|
+
self[ attribute ]
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
|
412
346
|
### Set attribute +attrname+ to a new +value+.
|
347
|
+
###
|
348
|
+
### @param [Symbol, String] attrname attribute name
|
349
|
+
### @param [Object] value the attribute value
|
413
350
|
def []=( attrname, value )
|
414
351
|
value = [ value ] unless value.is_a?( Array )
|
415
352
|
self.log.debug "Modifying %s to %p" % [ attrname, value ]
|
@@ -420,6 +357,9 @@ class Treequel::Branch
|
|
420
357
|
|
421
358
|
|
422
359
|
### Make the changes to the entry specified by the given +attributes+.
|
360
|
+
###
|
361
|
+
### @param attributes (see Treequel::Directory#modify)
|
362
|
+
### @return [TrueClass] if the merge succeeded
|
423
363
|
def merge( attributes )
|
424
364
|
self.directory.modify( self, attributes )
|
425
365
|
self.clear_caches
|
@@ -429,9 +369,34 @@ class Treequel::Branch
|
|
429
369
|
alias_method :modify, :merge
|
430
370
|
|
431
371
|
|
432
|
-
### Delete the
|
433
|
-
|
434
|
-
|
372
|
+
### Delete the specified attributes.
|
373
|
+
###
|
374
|
+
### @param [Array<Hash, #to_s>] attributes The attributes to delete, either as
|
375
|
+
### attribute names (in which case all values of the attribute are deleted) or
|
376
|
+
### Hashes of attributes and the Array of value/s which should be deleted.
|
377
|
+
###
|
378
|
+
### @example Delete all 'description' attributes
|
379
|
+
### branch.delete( :description )
|
380
|
+
### @example Delete the 'inetOrgPerson' and 'posixAccount' objectClasses from the entry
|
381
|
+
### branch.delete( :objectClass => [:inetOrgPerson, :posixAccount] )
|
382
|
+
### @example Delete any blank 'description' or 'cn' attributes:
|
383
|
+
### branch.delete( :description => '', :cn => '' )
|
384
|
+
###
|
385
|
+
### @return [TrueClass] if the delete succeeded
|
386
|
+
def delete( *attributes )
|
387
|
+
self.log.debug "Deleting attributes: %p" % [ attributes ]
|
388
|
+
mods = attributes.flatten.collect do |attribute|
|
389
|
+
if attribute.is_a?( Hash )
|
390
|
+
attribute.collect do |key,vals|
|
391
|
+
vals = Array( vals ).collect {|val| val.to_s }
|
392
|
+
LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, key.to_s, vals )
|
393
|
+
end
|
394
|
+
else
|
395
|
+
LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, [] )
|
396
|
+
end
|
397
|
+
end.flatten
|
398
|
+
|
399
|
+
self.directory.modify( self, mods )
|
435
400
|
self.clear_caches
|
436
401
|
|
437
402
|
return true
|
@@ -440,6 +405,8 @@ class Treequel::Branch
|
|
440
405
|
|
441
406
|
### Create the entry for this Branch with the specified +attributes+. The +attributes+ should,
|
442
407
|
### at a minimum, contain the pair `:objectClass => :someStructuralObjectClass`.
|
408
|
+
###
|
409
|
+
### @param [Hash<Symbol,String => Object>] attributes
|
443
410
|
def create( attributes={} )
|
444
411
|
self.directory.create( self, attributes )
|
445
412
|
return self
|
@@ -448,6 +415,10 @@ class Treequel::Branch
|
|
448
415
|
|
449
416
|
### Copy the entry for this Branch to a new entry with the given +newdn+ and merge in the
|
450
417
|
### specified +attributes+.
|
418
|
+
###
|
419
|
+
### @param [String] newdn the dn of the new entry
|
420
|
+
### @param [Hash<String, Symbol => Object>] attributes merge attributes
|
421
|
+
### @return [Treequel::Branch] a Branch for the new entry
|
451
422
|
def copy( newdn, attributes={} )
|
452
423
|
|
453
424
|
# Fully-qualify RDNs
|
@@ -468,14 +439,20 @@ class Treequel::Branch
|
|
468
439
|
### Move the entry associated with this branch to a new entry indicated by +rdn+. If
|
469
440
|
### any +attributes+ are given, also replace the corresponding attributes on the new
|
470
441
|
### entry with them.
|
471
|
-
|
442
|
+
###
|
443
|
+
### @param [String] rdn
|
444
|
+
### @param [Hash<String, Symbol => Object>] attributes
|
445
|
+
def move( rdn )
|
472
446
|
self.log.debug "Asking the directory to move me to an entry called %p" % [ rdn ]
|
473
|
-
return self.directory.move( self, rdn
|
447
|
+
return self.directory.move( self, rdn )
|
474
448
|
end
|
475
449
|
|
476
450
|
|
477
451
|
### Comparable interface: Returns -1 if other_branch is less than, 0 if other_branch is
|
478
452
|
### equal to, and +1 if other_branch is greater than the receiving Branch.
|
453
|
+
###
|
454
|
+
### @param [Treequel::Branch] other_branch
|
455
|
+
### @return [Fixnum]
|
479
456
|
def <=>( other_branch )
|
480
457
|
# Try the easy cases first
|
481
458
|
return nil unless other_branch.respond_to?( :dn ) &&
|
@@ -497,6 +474,9 @@ class Treequel::Branch
|
|
497
474
|
|
498
475
|
### Fetch a new Treequel::Branch object for the child of the receiver with the specified
|
499
476
|
### +rdn+.
|
477
|
+
###
|
478
|
+
### @param [String] rdn The RDN of the child to fetch.
|
479
|
+
### @return [Treequel::Branch]
|
500
480
|
def get_child( rdn )
|
501
481
|
newdn = [ rdn, self.dn ].join( ',' )
|
502
482
|
return self.class.new( self.directory, newdn )
|
@@ -505,41 +485,270 @@ class Treequel::Branch
|
|
505
485
|
|
506
486
|
### Addition operator: return a Treequel::BranchCollection that contains both the receiver
|
507
487
|
### and +other_branch+.
|
488
|
+
###
|
489
|
+
### @param [Treequel::Branch] other_branch
|
490
|
+
### @return [Treequel::BranchCollection]
|
508
491
|
def +( other_branch )
|
509
492
|
return Treequel::BranchCollection.new( self.branchset, other_branch.branchset )
|
510
493
|
end
|
511
494
|
|
512
495
|
|
496
|
+
### Return Treequel::Schema::ObjectClass instances for each of the receiver's
|
497
|
+
### objectClass attributes. If any +additional_classes+ are given,
|
498
|
+
### merge them with the current list of the current objectClasses for the lookup.
|
499
|
+
###
|
500
|
+
### @param [Array<String, Symbol>] additional_classes
|
501
|
+
### @return [Array<Treequel::Schema::ObjectClass>]
|
502
|
+
def object_classes( *additional_classes )
|
503
|
+
schema = self.directory.schema
|
504
|
+
|
505
|
+
oc_oids = self[:objectClass] || []
|
506
|
+
oc_oids |= additional_classes.collect {|str| str.to_sym }
|
507
|
+
oc_oids << :top if oc_oids.empty?
|
508
|
+
|
509
|
+
oclasses = []
|
510
|
+
oc_oids.each do |oid|
|
511
|
+
oc = schema.object_classes[ oid.to_sym ] or
|
512
|
+
raise Treequel::Error, "schema doesn't have a %p objectClass" % [ oid ]
|
513
|
+
oclasses << oc
|
514
|
+
end
|
515
|
+
|
516
|
+
return oclasses.uniq
|
517
|
+
end
|
518
|
+
|
519
|
+
|
520
|
+
### Return Treequel::Schema::AttributeType instances for each of the receiver's
|
521
|
+
### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
|
522
|
+
### include the MUST attributeTypes for them as well. This can be used to predict what
|
523
|
+
### attributes would need to be present for the entry to be saved if it added the
|
524
|
+
### +additional_object_classes+ to its own.
|
525
|
+
###
|
526
|
+
### @param [Array<String, Symbol>] additional_object_classes
|
527
|
+
### @return [Array<Treequel::Schema::AttributeType>]
|
528
|
+
def must_attribute_types( *additional_object_classes )
|
529
|
+
types = []
|
530
|
+
oclasses = self.object_classes( *additional_object_classes )
|
531
|
+
self.log.debug "Gathering MUST attribute types for objectClasses: %p" % [ oclasses ]
|
532
|
+
|
533
|
+
oclasses.each do |oc|
|
534
|
+
self.log.debug " adding %p from %p" % [ oc.must, oc ]
|
535
|
+
types |= oc.must
|
536
|
+
end
|
537
|
+
|
538
|
+
return types
|
539
|
+
end
|
540
|
+
|
541
|
+
|
542
|
+
### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
|
543
|
+
### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
|
544
|
+
### include the OIDs of the MUST attributes for them as well. This can be used to predict
|
545
|
+
### what attributes would need to be present for the entry to be saved if it added the
|
546
|
+
### +additional_object_classes+ to its own.
|
547
|
+
###
|
548
|
+
### @param [Array<String, Symbol>] additional_object_classes
|
549
|
+
### @return [Array<String, Symbol>] oid strings and symbols
|
550
|
+
def must_oids( *additional_object_classes )
|
551
|
+
return self.object_classes( *additional_object_classes ).
|
552
|
+
collect {|oc| oc.must_oids }.flatten.uniq.reject {|val| val == '' }
|
553
|
+
end
|
554
|
+
|
555
|
+
|
556
|
+
### Return a Hash of the attributes required by the Branch's objectClasses. If
|
557
|
+
### any +additional_object_classes+ are given, include the attributes that would be
|
558
|
+
### necessary for the entry to be saved with them.
|
559
|
+
###
|
560
|
+
### @param [Array<String, Symbol>] additional_object_classes
|
561
|
+
### @return [Hash<String => String>]
|
562
|
+
def must_attributes_hash( *additional_object_classes )
|
563
|
+
attrhash = {}
|
564
|
+
|
565
|
+
self.must_attribute_types( *additional_object_classes ).each do |attrtype|
|
566
|
+
self.log.debug " adding attrtype %p to the MUST attributes hash" % [ attrtype ]
|
567
|
+
|
568
|
+
if attrtype.name == :objectClass
|
569
|
+
attrhash[ :objectClass ] = [:top] | additional_object_classes
|
570
|
+
elsif attrtype.single?
|
571
|
+
attrhash[ attrtype.name ] = ''
|
572
|
+
else
|
573
|
+
attrhash[ attrtype.name ] = ['']
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
return attrhash
|
578
|
+
end
|
579
|
+
|
580
|
+
|
581
|
+
### Return Treequel::Schema::AttributeType instances for each of the receiver's
|
582
|
+
### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
|
583
|
+
### include the MAY attributeTypes for them as well. This can be used to predict what
|
584
|
+
### optional attributes could be added to the entry if the +additional_object_classes+
|
585
|
+
### were added to it.
|
586
|
+
###
|
587
|
+
### @param [Array<String, Symbol>] additional_object_classes
|
588
|
+
### @return [Array<Treequel::Schema::AttributeType>]
|
589
|
+
def may_attribute_types( *additional_object_classes )
|
590
|
+
return self.object_classes( *additional_object_classes ).
|
591
|
+
collect {|oc| oc.may }.flatten.uniq
|
592
|
+
end
|
593
|
+
|
594
|
+
|
595
|
+
### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
|
596
|
+
### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
|
597
|
+
### include the OIDs of the MAY attributes for them as well. This can be used to predict
|
598
|
+
### what optional attributes could be added to the entry if the +additional_object_classes+
|
599
|
+
### were added to it.
|
600
|
+
###
|
601
|
+
### @param [Array<String, Symbol>] additional_object_classes
|
602
|
+
### @return [Array<String, Symbol>] oid strings and symbols
|
603
|
+
def may_oids( *additional_object_classes )
|
604
|
+
return self.object_classes( *additional_object_classes ).
|
605
|
+
collect {|oc| oc.may_oids }.flatten.uniq
|
606
|
+
end
|
607
|
+
|
608
|
+
|
609
|
+
### Return a Hash of the optional attributes allowed by the Branch's objectClasses. If
|
610
|
+
### any +additional_object_classes+ are given, include the attributes that would be
|
611
|
+
### available for the entry if it had them.
|
612
|
+
###
|
613
|
+
### @param [Array<String, Symbol>] additional_object_classes
|
614
|
+
### @return [Hash<String => String>]
|
615
|
+
def may_attributes_hash( *additional_object_classes )
|
616
|
+
entry = self.entry
|
617
|
+
attrhash = {}
|
618
|
+
|
619
|
+
self.may_attribute_types( *additional_object_classes ).each do |attrtype|
|
620
|
+
self.log.debug " adding attrtype %p to the MAY attributes hash" % [ attrtype ]
|
621
|
+
|
622
|
+
if attrtype.single?
|
623
|
+
attrhash[ attrtype.name ] = nil
|
624
|
+
else
|
625
|
+
attrhash[ attrtype.name ] = []
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
attrhash[ :objectClass ] |= additional_object_classes
|
630
|
+
return attrhash
|
631
|
+
end
|
632
|
+
|
633
|
+
|
634
|
+
### Return Treequel::Schema::AttributeType instances for the set of all of the receiver's
|
635
|
+
### MUST and MAY attributeTypes.
|
636
|
+
###
|
637
|
+
### @return [Array<Treequel::Schema::AttributeType>]
|
638
|
+
def valid_attribute_types
|
639
|
+
return self.must_attribute_types | self.may_attribute_types
|
640
|
+
end
|
641
|
+
|
642
|
+
|
643
|
+
### Return a uniqified Array of OIDs (numeric OIDs as Strings, named OIDs as Symbols) for
|
644
|
+
### the set of all of the receiver's MUST and MAY attributeTypes.
|
645
|
+
###
|
646
|
+
### @return [Array<String, Symbol>]
|
647
|
+
def valid_attribute_oids
|
648
|
+
return self.must_oids | self.may_oids
|
649
|
+
end
|
650
|
+
|
651
|
+
|
652
|
+
### If the given +attroid+ is a valid attributeType name or numeric OID, return the
|
653
|
+
### AttributeType object that corresponds with it. If it isn't valid, return nil.
|
654
|
+
###
|
655
|
+
### @param [String,Symbol] attroid a numeric OID (as a String) or a named OID (as a Symbol)
|
656
|
+
### @return [Treequel::Schema::AttributeType] the validated attributeType
|
657
|
+
def valid_attribute_type( attroid )
|
658
|
+
return self.valid_attribute_types.find {|attr_type| attr_type.valid_name?(attroid) }
|
659
|
+
end
|
660
|
+
|
661
|
+
|
662
|
+
### Return +true+ if the specified +attrname+ is a valid attributeType given the
|
663
|
+
### receiver's current objectClasses.
|
664
|
+
###
|
665
|
+
### @param [String, Symbol] the OID (numeric or name) of the attribute in question
|
666
|
+
### @return [Boolean]
|
667
|
+
def valid_attribute?( attroid )
|
668
|
+
return !self.valid_attribute_type( attroid ).nil?
|
669
|
+
end
|
670
|
+
|
671
|
+
|
672
|
+
### Return a Hash of all the attributes allowed by the Branch's objectClasses. If
|
673
|
+
### any +additional_object_classes+ are given, include the attributes that would be
|
674
|
+
### available for the entry if it had them.
|
675
|
+
###
|
676
|
+
### @param [Array<String, Symbol>] additional_object_classes
|
677
|
+
### @return [Hash<String => String>]
|
678
|
+
def valid_attributes_hash( *additional_object_classes )
|
679
|
+
self.log.debug "Gathering a hash of all valid attributes:"
|
680
|
+
must = self.must_attributes_hash( *additional_object_classes )
|
681
|
+
self.log.debug " MUST attributes: %p" % [ must ]
|
682
|
+
may = self.may_attributes_hash( *additional_object_classes )
|
683
|
+
self.log.debug " MAY attributes: %p" % [ may ]
|
684
|
+
|
685
|
+
return may.merge( must )
|
686
|
+
end
|
687
|
+
|
513
688
|
|
514
689
|
#########
|
515
690
|
protected
|
516
691
|
#########
|
517
692
|
|
518
|
-
### Proxy method:
|
519
|
-
###
|
520
|
-
###
|
693
|
+
### Proxy method: call #traverse_branch if +attribute+ is a valid attribute
|
694
|
+
### and +value+ isn't +nil+.
|
695
|
+
### @see Treequel::Branch#traverse_branch
|
696
|
+
def method_missing( attribute, value=nil, additional_attributes={} )
|
697
|
+
return super( attribute ) if value.nil?
|
698
|
+
return self.traverse_branch( attribute, value, additional_attributes )
|
699
|
+
end
|
700
|
+
|
701
|
+
|
702
|
+
### If +attribute+ matches a valid attribute type in the directory's
|
703
|
+
### schema, return a new Branch for the RDN of +attribute+ and +value+, and
|
704
|
+
### +additional_attributes+ if it's a multi-value RDN.
|
705
|
+
###
|
706
|
+
### @param [Symbol] attribute the RDN attribute of the child
|
707
|
+
### @param [String] value the RDN valye of the child
|
708
|
+
### @param [Hash] additional_attributes any additional RDN attributes
|
521
709
|
###
|
522
|
-
###
|
710
|
+
### @example
|
523
711
|
### branch = Treequel::Branch.new( directory, 'ou=people,dc=acme,dc=com' )
|
524
712
|
### branch.uid( :chester ).dn
|
525
713
|
### # => 'uid=chester,ou=people,dc=acme,dc=com'
|
526
714
|
### branch.uid( :chester, :employeeType => 'admin' ).dn
|
527
715
|
### # => 'uid=chester+employeeType=admin,ou=people,dc=acme,dc=com'
|
528
|
-
|
529
|
-
|
716
|
+
###
|
717
|
+
### @return [Treequel::Branch] the Branch for the specified child
|
718
|
+
### @raise [NoMethodError] if the +attribute+ or any +additional_attributes+ are
|
719
|
+
### not valid attributeTypes.
|
720
|
+
def traverse_branch( attribute, value, additional_attributes={} )
|
530
721
|
valid_types = self.directory.schema.attribute_types
|
531
722
|
|
532
|
-
|
533
|
-
|
534
|
-
|
723
|
+
# Raise if either the primary attribute or any secondary attributes are invalid
|
724
|
+
if !valid_types.key?( attribute )
|
725
|
+
raise NoMethodError, "undefined method `%s' for %p" % [ attribute, self ]
|
726
|
+
elsif invalid = additional_attributes.keys.find {|ex_attr| !valid_types.key?(ex_attr) }
|
727
|
+
raise NoMethodError, "invalid secondary attribute `%s' for %p" %
|
728
|
+
[ invalid, self ]
|
729
|
+
end
|
535
730
|
|
731
|
+
# Make a normalized RDN from the arguments and return the Branch for it
|
536
732
|
rdn = rdn_from_pair_and_hash( attribute, value, additional_attributes )
|
537
|
-
|
538
733
|
return self.get_child( rdn )
|
539
734
|
end
|
540
735
|
|
541
736
|
|
737
|
+
### Fetch the entry from the Branch's directory.
|
738
|
+
def lookup_entry
|
739
|
+
self.log.debug "Looking up entry for %p" % [ self ]
|
740
|
+
if self.include_operational_attrs?
|
741
|
+
self.log.debug " including operational attributes."
|
742
|
+
return self.directory.get_extended_entry( self )
|
743
|
+
else
|
744
|
+
self.log.debug " not including operational attributes."
|
745
|
+
return self.directory.get_entry( self )
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
|
542
750
|
### Clear any cached values when the structural state of the object changes.
|
751
|
+
### @return [void]
|
543
752
|
def clear_caches
|
544
753
|
@entry = nil
|
545
754
|
@values.clear
|