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