treequel 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|