treequel 1.0.1 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +176 -14
- data/LICENSE +1 -1
- data/Rakefile +61 -45
- data/Rakefile.local +20 -0
- data/bin/treequel +502 -269
- data/examples/ldap-rack-auth.rb +2 -0
- data/lib/treequel.rb +221 -18
- data/lib/treequel/branch.rb +410 -201
- data/lib/treequel/branchcollection.rb +25 -13
- data/lib/treequel/branchset.rb +42 -40
- data/lib/treequel/constants.rb +233 -3
- data/lib/treequel/control.rb +95 -0
- data/lib/treequel/controls/contentsync.rb +138 -0
- data/lib/treequel/controls/pagedresults.rb +162 -0
- data/lib/treequel/controls/sortedresults.rb +216 -0
- data/lib/treequel/directory.rb +212 -65
- data/lib/treequel/exceptions.rb +11 -12
- data/lib/treequel/filter.rb +1 -12
- data/lib/treequel/mixins.rb +83 -47
- data/lib/treequel/monkeypatches.rb +29 -0
- data/lib/treequel/schema.rb +23 -19
- data/lib/treequel/schema/attributetype.rb +33 -3
- data/lib/treequel/schema/ldapsyntax.rb +0 -11
- data/lib/treequel/schema/matchingrule.rb +0 -11
- data/lib/treequel/schema/matchingruleuse.rb +0 -11
- data/lib/treequel/schema/objectclass.rb +36 -10
- data/lib/treequel/schema/table.rb +159 -0
- data/lib/treequel/sequel_integration.rb +7 -7
- data/lib/treequel/utils.rb +4 -66
- data/rake/documentation.rb +89 -0
- data/rake/helpers.rb +375 -307
- data/rake/hg.rb +16 -2
- data/rake/manual.rb +11 -6
- data/rake/packaging.rb +20 -35
- data/rake/publishing.rb +22 -62
- data/spec/lib/constants.rb +20 -0
- data/spec/lib/control_behavior.rb +44 -0
- data/spec/lib/matchers.rb +51 -0
- data/spec/treequel/branch_spec.rb +88 -29
- data/spec/treequel/branchcollection_spec.rb +24 -1
- data/spec/treequel/branchset_spec.rb +123 -51
- data/spec/treequel/control_spec.rb +48 -0
- data/spec/treequel/controls/contentsync_spec.rb +38 -0
- data/spec/treequel/controls/pagedresults_spec.rb +138 -0
- data/spec/treequel/controls/sortedresults_spec.rb +171 -0
- data/spec/treequel/directory_spec.rb +186 -16
- data/spec/treequel/mixins_spec.rb +42 -3
- data/spec/treequel/schema/attributetype_spec.rb +22 -20
- data/spec/treequel/schema/objectclass_spec.rb +67 -46
- data/spec/treequel/schema/table_spec.rb +134 -0
- data/spec/treequel_spec.rb +277 -15
- metadata +89 -108
- data/bin/treequel.orig +0 -963
- data/examples/ldap-monitor.rb +0 -143
- data/examples/ldap-monitor/public/css/master.css +0 -328
- data/examples/ldap-monitor/public/images/card_small.png +0 -0
- data/examples/ldap-monitor/public/images/chain_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
- data/examples/ldap-monitor/public/images/plug.png +0 -0
- data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
- data/examples/ldap-monitor/public/images/tick.png +0 -0
- data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
- data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
- data/examples/ldap-monitor/views/backends.erb +0 -41
- data/examples/ldap-monitor/views/connections.erb +0 -74
- data/examples/ldap-monitor/views/databases.erb +0 -39
- data/examples/ldap-monitor/views/dump_subsystem.erb +0 -14
- data/examples/ldap-monitor/views/index.erb +0 -14
- data/examples/ldap-monitor/views/layout.erb +0 -35
- data/examples/ldap-monitor/views/listeners.erb +0 -30
- data/rake/rdoc.rb +0 -30
- data/rake/win32.rb +0 -190
@@ -0,0 +1,95 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'ldap'
|
4
|
+
require 'ldap/control'
|
5
|
+
|
6
|
+
require 'treequel'
|
7
|
+
|
8
|
+
|
9
|
+
# Virtual interface methods for Control modules.
|
10
|
+
#
|
11
|
+
# @abstract To make a concrete derivative, include this module in a module that
|
12
|
+
# implements either {#get_client_controls} or {#get_server_controls} and #each.
|
13
|
+
# Your implementation of #each should +super+ with a block that does the
|
14
|
+
# necessary extraction of the result controls and yields back to the original
|
15
|
+
# block.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# module Treequel::MyControl
|
19
|
+
# include Treequel::Control
|
20
|
+
#
|
21
|
+
# # The control's OID
|
22
|
+
# OID = '1.3.6.1.4.1.984454.16.1'
|
23
|
+
#
|
24
|
+
# # If your control has some value associated with it, you can provide
|
25
|
+
# # an initializer to set up an instance variable or two.
|
26
|
+
# def initialize
|
27
|
+
# @my_control_value = 18
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# attr_accessor :my_control_value
|
31
|
+
#
|
32
|
+
# # This is the interface users will use to set values used in the control,
|
33
|
+
# # like so:
|
34
|
+
# # branchset.controlled_somehow( value )
|
35
|
+
# def controlled_somehow( value )
|
36
|
+
# self.my_control_value = value
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # This is overridden so you can fetch controls set by the server before
|
40
|
+
# # iterating. The #each in Treequel::Branchset will yield to this block
|
41
|
+
# # after performing a search.
|
42
|
+
# def each( &block )
|
43
|
+
# super do |branch|
|
44
|
+
# if my_control = branch.controls.find {|control| control.oid == OID }
|
45
|
+
# server_control_value = my_control.decode
|
46
|
+
# # ... do something with the returned server_control_value
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# block.call( branch )
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # This is how you inject your control into the search; Treequel::Branchset
|
54
|
+
# # will call this before running the search and add the results to its
|
55
|
+
# # server_controls. If you're implementing a client control, override
|
56
|
+
# # the #get_client_controls method instead. Be sure to super() so that any
|
57
|
+
# # controls registered before yours have a chance to add their objects too.
|
58
|
+
# def get_server_controls
|
59
|
+
# controls = super
|
60
|
+
# if self.my_control_value
|
61
|
+
# value = LDAP::Control.encode( self.my_control_value )
|
62
|
+
# controls << LDAP::Control.new( OID, value, true )
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# return controls
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
module Treequel::Control
|
71
|
+
|
72
|
+
### Control API interface method.
|
73
|
+
###
|
74
|
+
### If your control is a client control, you should super() to this method
|
75
|
+
### and add your control (in the form of an LDAP::Control object) to the
|
76
|
+
### resulting Array before returning it.
|
77
|
+
def get_client_controls
|
78
|
+
return []
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
### Control API interface method.
|
83
|
+
###
|
84
|
+
### If your control is a server control, you should super() to this method
|
85
|
+
### and add your control (in the form of an LDAP::Control object) to the
|
86
|
+
### resulting Array before returning it.
|
87
|
+
def get_server_controls
|
88
|
+
return []
|
89
|
+
end
|
90
|
+
|
91
|
+
end # module Treequel::Control
|
92
|
+
|
93
|
+
# vim: set nosta noet ts=4 sw=4:
|
94
|
+
|
95
|
+
|
@@ -0,0 +1,138 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
require 'ldap'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
require 'treequel'
|
8
|
+
require 'treequel/control'
|
9
|
+
require 'treequel/constants'
|
10
|
+
|
11
|
+
|
12
|
+
# A Treequel::Control module that implements the "Content Sync"
|
13
|
+
# control (RFC 4533)
|
14
|
+
#
|
15
|
+
# *NOTICE:* This control currently doesn't do anything, as it depends on
|
16
|
+
# Intermediate Responses, which the underlying Ruby-LDAP library doesn't
|
17
|
+
# support (yet).
|
18
|
+
#
|
19
|
+
# == Usage
|
20
|
+
#
|
21
|
+
# As with all Controls, you must first register the control with the
|
22
|
+
# Treequel::Directory object you're intending to search:
|
23
|
+
#
|
24
|
+
# dir = Treequel.directory( 'ldap://ldap.acme.com/dc=acme,dc=com' )
|
25
|
+
# dir.register_controls( Treequel::ContentSyncControl )
|
26
|
+
#
|
27
|
+
# Once that's done, any Treequel::Branchset you create will have the #on_sync
|
28
|
+
# method:
|
29
|
+
#
|
30
|
+
# # Build DHCP records out of all the hosts in the directory, then rebuild
|
31
|
+
# # everything when a host record changes.
|
32
|
+
# hosts = dir.filter( :ou => Hosts ).collection
|
33
|
+
# hosts.filter( :objectClass => :ipHost ).on_sync do ||
|
34
|
+
# #
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @see http://deveiate.org/projects/Treequel/ticket/6 Ticket: Add support for
|
38
|
+
# the RFC4533 Content Sync operation
|
39
|
+
#
|
40
|
+
module Treequel::ContentSyncControl
|
41
|
+
include Treequel::Control,
|
42
|
+
Treequel::Constants
|
43
|
+
|
44
|
+
# The control's OID
|
45
|
+
OID = CONTROL_OIDS[:sync]
|
46
|
+
|
47
|
+
# Sync mode constants (from RFC4533, section 2.2)
|
48
|
+
SYNC_MODE_REFRESH = 1
|
49
|
+
SYNC_MODE_REFRESH_AND_PERSIST = 3
|
50
|
+
|
51
|
+
### Add the requisite instance variables to including Branchsets.
|
52
|
+
def initialize
|
53
|
+
self.log.notice "The ContentSync control doesn't work yet -- it requires support for " +
|
54
|
+
"IntermediateResponses, which Ruby-LDAP doesn't do yet. " +
|
55
|
+
"See http://deveiate.org/projects/Treequel/ticket/6 for updates on the " +
|
56
|
+
"status of this."
|
57
|
+
@content_sync_callback = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
######
|
62
|
+
public
|
63
|
+
######
|
64
|
+
|
65
|
+
# The callback to call when results change
|
66
|
+
attr_accessor :content_sync_callback
|
67
|
+
|
68
|
+
|
69
|
+
### Clone the Branchset with a persistent change callback.
|
70
|
+
def on_sync( &callback )
|
71
|
+
newset = self.clone
|
72
|
+
newset.content_sync_callback = callback
|
73
|
+
|
74
|
+
return newset
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
### Override the Enumerable method to update the cookie value each time a page
|
79
|
+
### is fetched.
|
80
|
+
def each( &block )
|
81
|
+
super do |branch|
|
82
|
+
self.log.debug "Looking for the sync control in controls: %p" % [ branch.controls ]
|
83
|
+
branch.controls.each do |control|
|
84
|
+
self.log.debug " got a %s control: %p" % [
|
85
|
+
CONTROL_NAMES[control.oid],
|
86
|
+
control.decode,
|
87
|
+
]
|
88
|
+
|
89
|
+
case control.oid
|
90
|
+
when CONTROL_OIDS[:sync_state]
|
91
|
+
self.log.debug " got a 'state' control"
|
92
|
+
block.call( branch )
|
93
|
+
when CONTROL_OIDS[:sync_done]
|
94
|
+
self.log.debug " got a 'done' control"
|
95
|
+
break
|
96
|
+
else
|
97
|
+
self.log.info " got an unexpected control (%p)" % [ control ]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
#########
|
105
|
+
protected
|
106
|
+
#########
|
107
|
+
|
108
|
+
### Make the ASN.1 string for the control value out of the given +mode+,
|
109
|
+
### +cookie+, +reload_hint+.
|
110
|
+
def make_sync_control_value( mode, cookie, reload_hint )
|
111
|
+
# (http://tools.ietf.org/html/rfc4533#section-2.2):
|
112
|
+
# syncRequestValue ::= SEQUENCE {
|
113
|
+
# mode ENUMERATED {
|
114
|
+
# -- 0 unused
|
115
|
+
# refreshOnly (1),
|
116
|
+
# -- 2 reserved
|
117
|
+
# refreshAndPersist (3)
|
118
|
+
# },
|
119
|
+
# cookie syncCookie OPTIONAL,
|
120
|
+
# reloadHint BOOLEAN DEFAULT FALSE
|
121
|
+
# }
|
122
|
+
encoded_vals = [
|
123
|
+
OpenSSL::ASN1::Enumerated.new( SYNC_MODE_REFRESH_AND_PERSIST )
|
124
|
+
]
|
125
|
+
return OpenSSL::ASN1::Sequence.new( encoded_vals ).to_der
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
### Treequel::Control API -- Get a configured LDAP::Control object for this
|
130
|
+
### Branchset.
|
131
|
+
def get_server_controls
|
132
|
+
controls = super
|
133
|
+
value = self.make_sync_control_value( 1, '', false )
|
134
|
+
return controls << LDAP::Control.new( OID, value, true )
|
135
|
+
end
|
136
|
+
|
137
|
+
end # module Treequel::ContentSyncControl
|
138
|
+
|
@@ -0,0 +1,162 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
require 'ldap'
|
5
|
+
|
6
|
+
require 'treequel'
|
7
|
+
require 'treequel/control'
|
8
|
+
|
9
|
+
|
10
|
+
# A Treequel::Control module that implements the "LDAP Control Extension
|
11
|
+
# for Simple Paged Results Manipulation" (RFC 2696).
|
12
|
+
#
|
13
|
+
# == Usage
|
14
|
+
#
|
15
|
+
# As with all Controls, you must first register the control with the
|
16
|
+
# Treequel::Directory object you're intending to search:
|
17
|
+
#
|
18
|
+
# dir = Treequel.directory( 'ldap://ldap.acme.com/dc=acme,dc=com' )
|
19
|
+
# dir.register_controls( Treequel::PagedResultsControl )
|
20
|
+
#
|
21
|
+
# Once that's done, any Treequel::Branchset you create will have the
|
22
|
+
# #with_paged_results method that will allow you to specify the number
|
23
|
+
# of results you wish to be returned per "page":
|
24
|
+
#
|
25
|
+
# # Fetch people in pages
|
26
|
+
# people = dir.ou( :People )
|
27
|
+
# paged_people = people.filter( :objectClass => :person ).with_paged_results( 25 )
|
28
|
+
#
|
29
|
+
# The Branchset will also respond to #has_more_results?, which will
|
30
|
+
# be true while there are additional pages to be fetched, or before
|
31
|
+
# the search has taken place:
|
32
|
+
#
|
33
|
+
# # Display each set of 25, waiting for keypress between each set
|
34
|
+
# while paged_people.has_more_results?
|
35
|
+
# # do something with this set of 25 people...
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
module Treequel::PagedResultsControl
|
39
|
+
include Treequel::Control
|
40
|
+
|
41
|
+
# The control's OID
|
42
|
+
OID = '1.2.840.113556.1.4.319'
|
43
|
+
|
44
|
+
# The default number of results per page
|
45
|
+
DEFAULT_PAGE_SIZE = 100
|
46
|
+
|
47
|
+
|
48
|
+
### Add the control's instance variables to including Branchsets.
|
49
|
+
def initialize
|
50
|
+
@paged_results_cookie = ''
|
51
|
+
@paged_results_setsize = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
######
|
56
|
+
public
|
57
|
+
######
|
58
|
+
|
59
|
+
# The number of results per page
|
60
|
+
attr_accessor :paged_results_setsize
|
61
|
+
|
62
|
+
# The (opaque) cookie value that will be sent to the server on the next search.
|
63
|
+
attr_accessor :paged_results_cookie
|
64
|
+
|
65
|
+
|
66
|
+
### Clone the Branchset with a paged results control.
|
67
|
+
### @param [Fixnum] setsize The size of each result set. If this is nil or 0, removes
|
68
|
+
### paging from the Branchset.
|
69
|
+
### @return [Treequel::Branchset] a clone of the receiver with paging set to +setsize+
|
70
|
+
def with_paged_results( setsize=DEFAULT_PAGE_SIZE )
|
71
|
+
self.log.warn "This control will likely not work in ruby-ldap versions " +
|
72
|
+
" <= 0.9.9. See http://code.google.com/p/ruby-activeldap/issues/" +
|
73
|
+
"detail?id=38 for details." if LDAP::PATCH_VERSION < 10
|
74
|
+
|
75
|
+
newset = self.clone
|
76
|
+
|
77
|
+
if setsize.nil? || setsize.zero?
|
78
|
+
newset.paged_results_setsize = nil
|
79
|
+
else
|
80
|
+
newset.paged_results_setsize = setsize
|
81
|
+
end
|
82
|
+
|
83
|
+
return newset
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
### Clone the Branchset without paging and return it.
|
88
|
+
### @return [Treequel::Branchset] a clone of the receiver, stripped of its paging
|
89
|
+
def without_paging
|
90
|
+
copy = self.clone
|
91
|
+
copy.without_paging!
|
92
|
+
return copy
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
### Remove any paging control associated with the receiving Branchset.
|
97
|
+
### @return [void]
|
98
|
+
def without_paging!
|
99
|
+
self.paged_results_cookie = ''
|
100
|
+
self.paged_results_setsize = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
### Returns +true+ if the first page of results has been fetched and there are
|
105
|
+
### more pages remaining.
|
106
|
+
### @return [Boolean]
|
107
|
+
def has_more_results?
|
108
|
+
return true unless self.done_paging?
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
### Returns +true+ if results have yet to be fetched, or if they have all been
|
113
|
+
### fetched.
|
114
|
+
### @return [Boolean]
|
115
|
+
def done_paging?
|
116
|
+
return self.paged_results_cookie == ''
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
### Override the Enumerable method to update the cookie value each time a page
|
121
|
+
### is fetched.
|
122
|
+
### @yieldparam [Treequel::Branch] branch the branch that's a result of the search.
|
123
|
+
def each( &block )
|
124
|
+
super do |branch|
|
125
|
+
if paged_control = branch.controls.find {|control| control.oid == OID }
|
126
|
+
returned_size, cookie = paged_control.decode
|
127
|
+
self.log.debug "Paged control in result with size = %p, cookie = %p" %
|
128
|
+
[ returned_size, cookie ]
|
129
|
+
self.paged_results_cookie = cookie
|
130
|
+
else
|
131
|
+
self.log.debug "No paged control in results. Setting cookie to ''."
|
132
|
+
self.paged_results_cookie = ''
|
133
|
+
end
|
134
|
+
|
135
|
+
block.call( branch )
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
#########
|
141
|
+
protected
|
142
|
+
#########
|
143
|
+
|
144
|
+
### Treequel::Control API -- Get the set of server controls currently configured for
|
145
|
+
### the receiver.
|
146
|
+
### @return [Array<LDAP::Control>] the configured controls
|
147
|
+
def get_server_controls
|
148
|
+
controls = super
|
149
|
+
if pagesize = self.paged_results_setsize && self.paged_results_setsize.nonzero?
|
150
|
+
self.log.debug "Setting up paging for sets of %d" % [ pagesize ]
|
151
|
+
value = LDAP::Control.encode( pagesize.to_i, self.paged_results_cookie.to_s )
|
152
|
+
controls << LDAP::Control.new( OID, value, true )
|
153
|
+
else
|
154
|
+
self.log.debug "No paging for this %p; not adding the PagedResults control" %
|
155
|
+
[ self.class ]
|
156
|
+
end
|
157
|
+
|
158
|
+
return controls
|
159
|
+
end
|
160
|
+
|
161
|
+
end # module Treequel::PagedResultsControl
|
162
|
+
|
@@ -0,0 +1,216 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
require 'ldap'
|
5
|
+
|
6
|
+
require 'treequel'
|
7
|
+
require 'treequel/control'
|
8
|
+
require 'treequel/sequel_integration'
|
9
|
+
|
10
|
+
|
11
|
+
# A Treequel::Control module that implements the "LDAP Control Extension for Server
|
12
|
+
# Side Sorting of Search Results" (RFC 2891).
|
13
|
+
#
|
14
|
+
# == Usage
|
15
|
+
#
|
16
|
+
# As with all Controls, you must first register the control with the
|
17
|
+
# Treequel::Directory object you're intending to search:
|
18
|
+
#
|
19
|
+
# dir = Treequel.directory( 'ldap://ldap.acme.com/dc=acme,dc=com' )
|
20
|
+
# dir.register_controls( Treequel::SortedResultsControl )
|
21
|
+
#
|
22
|
+
# Once that's done, any Treequel::Branchset you create will have the
|
23
|
+
# #order method that will allow you to specify one or more attributes by which the server
|
24
|
+
# should sort results before returning them:
|
25
|
+
#
|
26
|
+
# # Fetch people sorted by their last name, then first name, then
|
27
|
+
# # by employeeNumber
|
28
|
+
# people = dir.ou( :People )
|
29
|
+
# sorted_people = people.filter( :objectClass => :person ).
|
30
|
+
# order( :sn, :givenName, :employeeNumber )
|
31
|
+
module Treequel::SortedResultsControl
|
32
|
+
include Treequel::Control,
|
33
|
+
Treequel::Constants
|
34
|
+
|
35
|
+
# The control's request OID
|
36
|
+
OID = CONTROL_OIDS[:sortrequest]
|
37
|
+
|
38
|
+
# The control's response OID
|
39
|
+
RESPONSE_OID = CONTROL_OIDS[:sortresult]
|
40
|
+
|
41
|
+
# Descriptions of result codes
|
42
|
+
RESPONSE_RESULT_DESCRIPTIONS = {
|
43
|
+
# success (0), -- results are sorted
|
44
|
+
0 => 'Success',
|
45
|
+
|
46
|
+
# operationsError (1), -- server internal failure
|
47
|
+
1 => 'Operations error; server internal failure',
|
48
|
+
|
49
|
+
# timeLimitExceeded (3), -- timelimit reached before
|
50
|
+
# -- sorting was completed
|
51
|
+
3 => 'Time limit reached before sorting was completed',
|
52
|
+
|
53
|
+
# strongAuthRequired (8), -- refused to return sorted
|
54
|
+
# -- results via insecure
|
55
|
+
# -- protocol
|
56
|
+
8 => 'Stronger auth required: refusing to return sorted results via insecure protocol',
|
57
|
+
|
58
|
+
# adminLimitExceeded (11), -- too many matching entries
|
59
|
+
# -- for the server to sort
|
60
|
+
11 => 'Admin limit exceeded: too many matching entries for the server to sort',
|
61
|
+
|
62
|
+
# noSuchAttribute (16), -- unrecognized attribute
|
63
|
+
# -- type in sort key
|
64
|
+
16 => 'No such attribute',
|
65
|
+
|
66
|
+
# inappropriateMatching (18), -- unrecognized or
|
67
|
+
# -- inappropriate matching
|
68
|
+
# -- rule in sort key
|
69
|
+
18 => 'Inappropriate matching: unrecognized or inappropriate matching rule in sort key',
|
70
|
+
|
71
|
+
# insufficientAccessRights (50), -- refused to return sorted
|
72
|
+
# -- results to this client
|
73
|
+
50 => 'Insufficient access rights: refusing to return sorted results to this client',
|
74
|
+
|
75
|
+
# busy (51), -- too busy to process
|
76
|
+
51 => 'Busy: too busy to process',
|
77
|
+
|
78
|
+
# unwillingToPerform (53), -- unable to sort
|
79
|
+
52 => 'Unwilling to perform: unable to sort',
|
80
|
+
|
81
|
+
# other (80)
|
82
|
+
80 => 'Other: non-specific result code (80)',
|
83
|
+
}
|
84
|
+
|
85
|
+
|
86
|
+
# A struct for tracking sorting criteria
|
87
|
+
Criterion = Struct.new( 'SortedResultsControlCriterion', :type, :ordering_rule, :reverse_order )
|
88
|
+
|
89
|
+
|
90
|
+
### Add the requisite instance variables to including Branchsets.
|
91
|
+
def initialize
|
92
|
+
self.log.debug "initializing %p" % [ self ]
|
93
|
+
@sort_order_criteria = []
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
######
|
98
|
+
public
|
99
|
+
######
|
100
|
+
|
101
|
+
# The ordering criteria, if any
|
102
|
+
attr_accessor :sort_order_criteria
|
103
|
+
|
104
|
+
|
105
|
+
### Clone the Branchset with a server-side sorted results control added and return it.
|
106
|
+
def order( *attributes )
|
107
|
+
self.log.warn "This control will likely not work in ruby-ldap versions " +
|
108
|
+
" <= 0.9.9. See http://code.google.com/p/ruby-activeldap/issues/" +
|
109
|
+
"detail?id=38 for details." if LDAP::PATCH_VERSION < 10
|
110
|
+
|
111
|
+
if attributes.flatten.empty?
|
112
|
+
self.log.debug "cloning %p with no order" % [ self ]
|
113
|
+
return self.unordered
|
114
|
+
else
|
115
|
+
criteria = attributes.collect do |attrspec|
|
116
|
+
case attrspec
|
117
|
+
when Symbol
|
118
|
+
Criterion.new( attrspec.to_s )
|
119
|
+
|
120
|
+
when Sequel::SQL::Expression
|
121
|
+
Criterion.new( attrspec.expression.to_s, nil, attrspec.descending )
|
122
|
+
|
123
|
+
else
|
124
|
+
raise ArgumentError,
|
125
|
+
"unsupported order specification type %s" % [ attrspec.class.name ]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
self.log.debug "cloning %p with order criteria: %p" % [ self, criteria ]
|
130
|
+
copy = self.clone
|
131
|
+
copy.sort_order_criteria += criteria
|
132
|
+
|
133
|
+
return copy
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
### Clone the Branchset without a server-side sorted results control and return it.
|
139
|
+
def unordered
|
140
|
+
copy = self.clone
|
141
|
+
copy.unordered!
|
142
|
+
return copy
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
### Remove any server-side sorted results control associated with the receiving
|
147
|
+
### Branchset, returning any removed criteria as an Array.
|
148
|
+
def unordered!
|
149
|
+
return self.sort_order_criteria.slice!( 0..-1 )
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
### Override the Enumerable method to update the cookie value each time a page
|
154
|
+
### is fetched.
|
155
|
+
def each( &block )
|
156
|
+
super do |branch|
|
157
|
+
if sorted_control = branch.controls.find {|control| control.oid == RESPONSE_OID }
|
158
|
+
sortResult, attributeType = sorted_control.decode
|
159
|
+
if sortResult.nonzero?
|
160
|
+
self.log.error "got non-zero response code for sort: %d (%s)" %
|
161
|
+
[ sortResult, RESPONSE_RESULT_DESCRIPTIONS[sortResult] ]
|
162
|
+
raise Treequel::ControlError, RESPONSE_RESULT_DESCRIPTIONS[sortResult]
|
163
|
+
else
|
164
|
+
self.log.debug "got 'success' sort response code."
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
block.call( branch )
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
#########
|
174
|
+
protected
|
175
|
+
#########
|
176
|
+
|
177
|
+
### Make the ASN.1 string for the control value out of the given +mode+,
|
178
|
+
### +cookie+, +reload_hint+.
|
179
|
+
def make_sorted_control_value( sort_criteria )
|
180
|
+
|
181
|
+
### (http://tools.ietf.org/html/rfc2891#section-1.1):
|
182
|
+
# SortKeyList ::= SEQUENCE OF SEQUENCE {
|
183
|
+
# attributeType AttributeDescription,
|
184
|
+
# orderingRule [0] MatchingRuleId OPTIONAL,
|
185
|
+
# reverseOrder [1] BOOLEAN DEFAULT FALSE }
|
186
|
+
encoded_vals = sort_criteria.collect do |criterion|
|
187
|
+
seq = []
|
188
|
+
seq << OpenSSL::ASN1::OctetString.new( criterion.type )
|
189
|
+
seq << OpenSSL::ASN1::ObjectId.new( criterion.ordering_rule ) if criterion.ordering_rule
|
190
|
+
seq << OpenSSL::ASN1::Boolean.new( true ) if criterion.reverse_order
|
191
|
+
OpenSSL::ASN1::Sequence.new( seq )
|
192
|
+
end
|
193
|
+
|
194
|
+
return OpenSSL::ASN1::Sequence.new( encoded_vals ).to_der
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
### Treequel::Control API -- Get a configured LDAP::Control object for this
|
199
|
+
### Branchset.
|
200
|
+
def get_server_controls
|
201
|
+
controls = super
|
202
|
+
criteria = self.sort_order_criteria
|
203
|
+
|
204
|
+
if criteria.empty?
|
205
|
+
self.log.debug "No sort order criteria; skipping the server-side sort control"
|
206
|
+
else
|
207
|
+
self.log.debug "Found %d sort order criteria; generating a server-side sort control" %
|
208
|
+
[ criteria.length ]
|
209
|
+
asn1_string = self.make_sorted_control_value( criteria )
|
210
|
+
controls << LDAP::Control.new( OID, asn1_string, true )
|
211
|
+
end
|
212
|
+
|
213
|
+
return controls
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|