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