ruby-ldapserver 0.5.3 → 0.7.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.
@@ -31,11 +31,11 @@ class Server
31
31
  def log(msg, severity = Logger::INFO)
32
32
  @logger.add(severity, msg, @io.peeraddr[3])
33
33
  end
34
-
34
+
35
35
  def debug msg
36
36
  log msg, Logger::DEBUG
37
37
  end
38
-
38
+
39
39
  def log_exception(e)
40
40
  log "#{e}: #{e.backtrace.join("\n\tfrom ")}", Logger::ERROR
41
41
  end
@@ -93,8 +93,6 @@ class Server
93
93
  end
94
94
 
95
95
  def handle_requests
96
- operationClass = @opt[:operation_class]
97
- ocArgs = @opt[:operation_args] || []
98
96
  catch(:close) do
99
97
  while true
100
98
  begin
@@ -120,9 +118,14 @@ class Server
120
118
  case protocolOp.tag
121
119
  when 0 # BindRequest
122
120
  abandon_all
123
- @binddn, @version = operationClass.new(self,messageId,*ocArgs).
124
- do_bind(protocolOp, controls)
125
-
121
+ if @opt[:router]
122
+ @binddn, @version = @opt[:router].do_bind(self, messageId, protocolOp, controls)
123
+ else
124
+ operationClass = @opt[:operation_class]
125
+ ocArgs = @opt[:operation_args] || []
126
+ @binddn, @version = operationClass.new(self,messageId,*ocArgs).
127
+ do_bind(protocolOp, controls)
128
+ end
126
129
  when 2 # UnbindRequest
127
130
  throw(:close)
128
131
 
@@ -171,14 +174,17 @@ class Server
171
174
  # client sends an overlapping request with same message ID,
172
175
  # so we don't have to worry about the case where there is
173
176
  # already a thread with this messageId in @threadgroup.
174
-
175
177
  def start_op(messageId,protocolOp,controls,meth)
176
178
  operationClass = @opt[:operation_class]
177
179
  ocArgs = @opt[:operation_args] || []
178
180
  thr = Thread.new do
179
181
  begin
180
- operationClass.new(self,messageId,*ocArgs).
181
- send(meth,protocolOp,controls)
182
+ if @opt[:router]
183
+ @opt[:router].send meth, self, messageId, protocolOp, controls
184
+ else
185
+ operationClass.new(self,messageId,*ocArgs).
186
+ send(meth,protocolOp,controls)
187
+ end
182
188
  rescue Exception => e
183
189
  log_exception e
184
190
  end
@@ -0,0 +1,220 @@
1
+ require 'ldap/server/util'
2
+
3
+ module LDAP
4
+ class Server
5
+
6
+ class DN
7
+ include Enumerable
8
+
9
+ attr_reader :dname
10
+
11
+ # Combines a set of elements to a syntactically correct DN
12
+ # elements is [elements, ...] where elements
13
+ # can be either { attr => val } or [attr, val]
14
+ def self.join(elements)
15
+ LDAP::Server::Operation.join_dn(elements)
16
+ end
17
+
18
+ def initialize(dn)
19
+ @dname = LDAP::Server::Operation.split_dn(dn)
20
+ end
21
+
22
+ # Returns the value of the first occurrence of attr (bottom-up)
23
+ def find_first(attr)
24
+ @dname.each do |pair|
25
+ return pair[attr.to_s] if pair[attr.to_s]
26
+ end
27
+ nil
28
+ end
29
+
30
+ # Returns the value of the last occurrence of attr (bottom-up)
31
+ def find_last(attr)
32
+ @dname.reverse_each do |pair|
33
+ return pair[attr.to_s] if pair[attr.to_s]
34
+ end
35
+ nil
36
+ end
37
+
38
+ # Returns all values of all occurrences of attr (bottom-up)
39
+ def find(attr)
40
+ result = []
41
+ @dname.each do |pair|
42
+ result << pair[attr.to_s] if pair[attr.to_s]
43
+ end
44
+ result
45
+ end
46
+
47
+ # Returns the value of the n-th occurrence of attr (top-down, 0 is first element)
48
+ def find_nth(attr, n)
49
+ i = 0
50
+ @dname.each do |pair|
51
+ if pair[attr.to_s]
52
+ return pair[attr.to_s] if i == n
53
+ i += 1
54
+ end
55
+ end
56
+ nil
57
+ end
58
+
59
+ # Whether or not the DN starts with dn (bottom-up)
60
+ # dn is a string
61
+ def start_with?(dn)
62
+ needle = LDAP::Server::Operation.split_dn(dn)
63
+
64
+ # Needle is longer than haystack
65
+ return false if needle.length > @dname.length
66
+
67
+ needle_index = 0
68
+ haystack_index = 0
69
+
70
+ while needle_index < needle.length
71
+ return false if @dname[haystack_index] != needle[needle_index]
72
+ needle_index += 1
73
+ haystack_index += 1
74
+ end
75
+ true
76
+ end
77
+
78
+ # Whether or not the DN starts with a format (bottom-up) (values are ignored)
79
+ # dn is a string
80
+ def start_with_format?(dn)
81
+ needle = LDAP::Server::Operation.split_dn(dn)
82
+
83
+ # Needle is longer than haystack
84
+ return false if needle.length > @dname.length
85
+
86
+ needle_index = 0
87
+ haystack_index = 0
88
+
89
+ while needle_index < needle.length
90
+ return false if @dname[haystack_index].keys != needle[needle_index].keys
91
+ needle_index += 1
92
+ haystack_index += 1
93
+ end
94
+ true
95
+ end
96
+
97
+ # Whether or not the DN ends with dn (top-down)
98
+ # dn is a string
99
+ def end_with?(dn)
100
+ needle = LDAP::Server::Operation.split_dn(dn)
101
+
102
+ # Needle is longer than haystack
103
+ return false if needle.length > @dname.length
104
+
105
+ needle_index = needle.length - 1
106
+ haystack_index = @dname.length - 1
107
+
108
+ while needle_index >= 0
109
+ return false if @dname[haystack_index] != needle[needle_index]
110
+ needle_index -= 1
111
+ haystack_index -= 1
112
+ end
113
+ true
114
+ end
115
+
116
+ # Whether or not the DN ends with format (top-down) (values are ignored)
117
+ # dn is a string
118
+ def end_with_format?(dn)
119
+ needle = LDAP::Server::Operation.split_dn(dn)
120
+
121
+ # Needle is longer than haystack
122
+ return false if needle.length > @dname.length
123
+
124
+ needle_index = needle.length - 1
125
+ haystack_index = @dname.length - 1
126
+
127
+ while needle_index >= 0
128
+ return false if @dname[haystack_index].keys != needle[needle_index].keys
129
+ needle_index -= 1
130
+ haystack_index -= 1
131
+ end
132
+ true
133
+ end
134
+
135
+ # Whether or not the DN equals dn (values are case sensitive)
136
+ # dn is a string
137
+ def equal?(dn)
138
+ split_dn = LDAP::Server::Operation.split_dn(dn)
139
+
140
+ return false if split_dn.length != @dname.length
141
+
142
+ @dname.each_with_index do |pair, index|
143
+ return false if pair != split_dn[index]
144
+ end
145
+ true
146
+ end
147
+
148
+ # Whether or not the DN equals dn's format (values are ignored) (case insensitive)
149
+ # dn is a string
150
+ def equal_format?(dn)
151
+ split_dn = LDAP::Server::Operation.split_dn(dn)
152
+
153
+ return false if split_dn.length != @dname.length
154
+
155
+ @dname.each_with_index do |pair, index|
156
+ return false if pair.keys != split_dn[index].keys
157
+ end
158
+ true
159
+ end
160
+
161
+ # Whether or not the DN constains a substring equal to dn (values are case sensitive)
162
+ # dn is a string
163
+ def include?(dn)
164
+ split_dn = LDAP::Server::Operation.split_dn(dn)
165
+ return false if split_dn.length > @dname.length
166
+ LDAP::Server::Operation.join_dn(@dname).include?(LDAP::Server::Operation.join_dn(split_dn))
167
+ end
168
+
169
+ # Whether or not the DN constains a substring format equal to dn (values are ignored) (case insensitive)
170
+ # dn is a string
171
+ def include_format?(dn)
172
+ split_dn = LDAP::Server::Operation.split_dn(dn)
173
+
174
+ return false if split_dn.length > @dname.length
175
+
176
+ haystack = []
177
+ @dname.each { |pair| haystack << pair.keys }
178
+
179
+ needle = []
180
+ split_dn.each { |pair| needle << pair.keys }
181
+
182
+ haystack.join.include?(needle.join)
183
+ end
184
+
185
+ # Generates a mapping for variables
186
+ # For example:
187
+ # > dn = LDAP::Server.DN.new("uid=user,ou=Users,dc=mydomain,dc=com")
188
+ # > dn.parse("uid=:uid, ou=:category, dc=mydomain, dc=com")
189
+ # => { :uid => "user", :category => "Users" }
190
+ def parse(template_dn)
191
+ result = {}
192
+ template = LDAP::Server::Operation.split_dn(template_dn)
193
+ template.reverse.zip(@dname.reverse).each do |temp, const|
194
+ break if const and temp.keys.first != const.keys.first
195
+ if temp.values.first.start_with?(':')
196
+ sym = temp.values.first[1..-1].to_sym
197
+ if const
198
+ result[sym] = const.values.first unless result[sym]
199
+ else
200
+ result[sym] = nil
201
+ end
202
+ elsif temp.values.first != const.values.first
203
+ break
204
+ end
205
+ end
206
+ result
207
+ end
208
+
209
+ def each(&block)
210
+ @dname.each(&block)
211
+ end
212
+
213
+ def reverse_each(&block)
214
+ @dname.reverse_each(&block)
215
+ end
216
+
217
+ end
218
+
219
+ end
220
+ end
@@ -202,7 +202,7 @@ class Server
202
202
  when :eq, :approx, :le, :ge, :substrings
203
203
  # the filter now includes a suitable matching object
204
204
  return (filter[2] || LDAP::Server::MatchingRule::DefaultMatch).send(
205
- filter.first, av[filter[1].to_s], *filter[3..-1])
205
+ filter.first, Array(av[filter[1].to_s]), *filter[3..-1])
206
206
 
207
207
  when :true
208
208
  return true
@@ -1,4 +1,5 @@
1
1
  require 'timeout'
2
+ require 'openssl'
2
3
  require 'ldap/server/result'
3
4
  require 'ldap/server/filter'
4
5
 
@@ -36,6 +37,7 @@ class Server
36
37
  ])
37
38
  @schema = @connection.opt[:schema]
38
39
  @server = @connection.opt[:server]
40
+ @attribute_range_limit = @connection.opt[:attribute_range_limit]
39
41
  end
40
42
 
41
43
  def log msg, severity = Logger::INFO
@@ -45,9 +47,9 @@ class Server
45
47
  def debug msg
46
48
  @connection.debug msg
47
49
  end
48
-
50
+
49
51
  # Send an exception report to the log
50
-
52
+
51
53
  def log_exception msg
52
54
  @connection.log_exception msg
53
55
  end
@@ -84,7 +86,7 @@ class Server
84
86
  seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
85
87
  end
86
88
  yield seq if block_given? # opportunity to add more elements
87
-
89
+
88
90
  send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
89
91
  end
90
92
 
@@ -96,6 +98,9 @@ class Server
96
98
  end
97
99
  end
98
100
 
101
+
102
+ AttributeRange = Struct.new :start, :end
103
+
99
104
  # Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
100
105
  # If schema given, return operational attributes only if
101
106
  # explicitly requested
@@ -114,23 +119,43 @@ class Server
114
119
  sendall = @attributes == [] || @attributes.include?("*")
115
120
  avseq = []
116
121
 
117
- avs.each do |attr, vals|
118
- if !@attributes.include?(attr)
122
+ avs.each_with_index do |(attr, vals), aidx|
123
+ query_attr_idx = @attributes.index(attr)
124
+ if !query_attr_idx
119
125
  next unless sendall
120
126
  if @schema
121
127
  a = @schema.find_attrtype(attr)
122
128
  next unless a and (a.usage.nil? or a.usage == :userApplications)
123
129
  end
124
130
  end
131
+ query_attr = query_attr_idx && @attribute_ranges[query_attr_idx]
125
132
 
126
133
  if @typesOnly
127
- vals = []
134
+ vals = []
128
135
  else
129
136
  vals = [vals] unless vals.kind_of?(Array)
130
137
  # FIXME: optionally do a value_to_s conversion here?
131
138
  # FIXME: handle attribute;binary
132
139
  end
133
140
 
141
+ if (@attribute_range_limit && vals.size > @attribute_range_limit) || query_attr&.start
142
+ if query_attr&.start
143
+ range_start = query_attr.start.to_i
144
+ range_end = query_attr.end == "*" ? -1 : query_attr.end.to_i
145
+ else
146
+ range_start = 0
147
+ range_end = @attribute_range_limit ? @attribute_range_limit - 1 : -1
148
+ end
149
+ range_end = range_start + @attribute_range_limit - 1 if @attribute_range_limit && (vals.size - range_start > @attribute_range_limit)
150
+ range_end = -1 if vals.size <= range_end
151
+ rvals = vals[range_start .. range_end]
152
+ vals = []
153
+ avseq << OpenSSL::ASN1::Sequence([
154
+ OpenSSL::ASN1::OctetString("#{attr};range=#{range_start}-#{range_end == -1 ? "*" : range_end}"),
155
+ OpenSSL::ASN1::Set(rvals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
156
+ ])
157
+ end
158
+
134
159
  avseq << OpenSSL::ASN1::Sequence([
135
160
  OpenSSL::ASN1::OctetString(attr),
136
161
  OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
@@ -200,8 +225,8 @@ class Server
200
225
  when 0
201
226
  simple_bind(version, dn, authentication.value)
202
227
  when 3
203
- mechanism = authentication.value[0].value
204
- credentials = authentication.value[1].value
228
+ # mechanism = authentication.value[0].value
229
+ # credentials = authentication.value[1].value
205
230
  # sasl_bind(version, dn, mechanism, credentials)
206
231
  # FIXME: needs to exchange further BindRequests
207
232
  raise LDAP::ResultError::AuthMethodNotSupported
@@ -246,7 +271,20 @@ class Server
246
271
  client_timelimit = protocolOp.value[4].value.to_i
247
272
  @typesOnly = protocolOp.value[5].value
248
273
  filter = Filter::parse(protocolOp.value[6], @schema)
249
- @attributes = protocolOp.value[7].value.collect {|x| x.value}
274
+ attributes = protocolOp.value[7].value.collect {|x| x.value}
275
+ attributes = attributes.map do |attr|
276
+ if attr =~ /(.*);range=(\d+)-(\d+|\*)\z/
277
+ [$1, $2, $3]
278
+ else
279
+ attr
280
+ end
281
+ end
282
+ @attributes = attributes.map do |name, |
283
+ name
284
+ end
285
+ @attribute_ranges = attributes.map do |_, range_start, range_end|
286
+ range_start && AttributeRange.new(range_start, range_end)
287
+ end
250
288
 
251
289
  @rescount = 0
252
290
  @sizelimit = server_sizelimit
@@ -0,0 +1,166 @@
1
+ require 'openssl'
2
+
3
+ module LDAP
4
+ class Server
5
+
6
+ class Request
7
+ attr_accessor :connection, :typesOnly, :attributes, :rescount, :sizelimit
8
+
9
+ # Object to handle a single LDAP request. This object is created on
10
+ # every request by the router, and is passed as argument to the defined
11
+ # routes.
12
+
13
+ def initialize(connection, messageId)
14
+ @connection = connection
15
+ @respEnvelope = OpenSSL::ASN1::Sequence([
16
+ OpenSSL::ASN1::Integer(messageId),
17
+ # protocolOp,
18
+ # controls [0] OPTIONAL,
19
+ ])
20
+ @schema = @connection.opt[:schema]
21
+ @server = @connection.opt[:server]
22
+ @rescount = 0
23
+ end
24
+
25
+ ##################################################
26
+ ### Utility methods to send protocol responses ###
27
+ ##################################################
28
+
29
+ def send_LDAPMessage(protocolOp, opt={}) # :nodoc:
30
+ @respEnvelope.value[1] = protocolOp
31
+ if opt[:controls]
32
+ @respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION)
33
+ else
34
+ @respEnvelope.value.delete_at(2)
35
+ end
36
+
37
+ @connection.write(@respEnvelope.to_der)
38
+ end
39
+
40
+ def send_LDAPResult(tag, resultCode, opt={}) # :nodoc:
41
+ seq = [
42
+ OpenSSL::ASN1::Enumerated(resultCode),
43
+ OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
44
+ OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
45
+ ]
46
+ if opt[:referral]
47
+ rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
48
+ seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
49
+ end
50
+ yield seq if block_given? # opportunity to add more elements
51
+
52
+ send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
53
+ end
54
+
55
+ def send_BindResponse(resultCode, opt={})
56
+ send_LDAPResult(1, resultCode, opt) do |resp|
57
+ if opt[:serverSaslCreds]
58
+ resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
64
+ # If schema given, return operational attributes only if
65
+ # explicitly requested
66
+
67
+ def send_SearchResultEntry(dn, avs, opt={})
68
+ @rescount += 1
69
+ if @sizelimit
70
+ raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit
71
+ end
72
+
73
+ if @schema
74
+ # normalize the attribute names
75
+ @attributes = @attributes.map { |a| a == '*' ? a : @schema.find_attrtype(a).to_s }
76
+ end
77
+
78
+ sendall = @attributes == [] || @attributes.include?("*")
79
+ avseq = []
80
+
81
+ avs.each do |attr, vals|
82
+ if !@attributes.include?(attr)
83
+ next unless sendall
84
+ if @schema
85
+ a = @schema.find_attrtype(attr)
86
+ next unless a and (a.usage.nil? or a.usage == :userApplications)
87
+ end
88
+ end
89
+
90
+ if @typesOnly
91
+ vals = []
92
+ else
93
+ vals = [vals] unless vals.kind_of?(Array)
94
+ # FIXME: optionally do a value_to_s conversion here?
95
+ # FIXME: handle attribute;binary
96
+ end
97
+
98
+ avseq << OpenSSL::ASN1::Sequence([
99
+ OpenSSL::ASN1::OctetString(attr),
100
+ OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
101
+ ])
102
+ end
103
+
104
+ send_LDAPMessage(OpenSSL::ASN1::Sequence([
105
+ OpenSSL::ASN1::OctetString(dn),
106
+ OpenSSL::ASN1::Sequence(avseq),
107
+ ], 4, :IMPLICIT, :APPLICATION), opt)
108
+ end
109
+
110
+ def send_SearchResultDone(resultCode, opt={})
111
+ send_LDAPResult(5, resultCode, opt)
112
+ end
113
+
114
+ def send_ModifyResponse(resultCode, opt={})
115
+ send_LDAPResult(7, resultCode, opt)
116
+ end
117
+
118
+ def send_AddResponse(resultCode, opt={})
119
+ send_LDAPResult(9, resultCode, opt)
120
+ end
121
+
122
+ def send_DelResponse(resultCode, opt={})
123
+ send_LDAPResult(11, resultCode, opt)
124
+ end
125
+
126
+ def send_ModifyDNResponse(resultCode, opt={})
127
+ send_LDAPResult(13, resultCode, opt)
128
+ end
129
+
130
+ def send_CompareResponse(resultCode, opt={})
131
+ send_LDAPResult(15, resultCode, opt)
132
+ end
133
+
134
+ def send_ExtendedResponse(resultCode, opt={})
135
+ send_LDAPResult(24, resultCode, opt) do |resp|
136
+ if opt[:responseName]
137
+ resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
138
+ end
139
+ if opt[:response]
140
+ resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
141
+ end
142
+ end
143
+ end
144
+
145
+ ############################################################
146
+ ### Methods to get parameters related to this connection ###
147
+ ############################################################
148
+
149
+ # Server-set maximum time limit. Override for more complex behaviour
150
+ # (e.g. limit depends on @connection.binddn). Nil uses hardcoded default.
151
+
152
+ def server_timelimit
153
+ @connection.opt[:timelimit]
154
+ end
155
+
156
+ # Server-set maximum size limit. Override for more complex behaviour
157
+ # (e.g. limit depends on @connection.binddn). Return nil for unlimited.
158
+
159
+ def server_sizelimit
160
+ @connection.opt[:sizelimit]
161
+ end
162
+
163
+ end
164
+
165
+ end
166
+ end
@@ -63,7 +63,7 @@ class ResultError
63
63
  53 => UnwillingToPerform,
64
64
  # FIXME: please fill in the rest
65
65
  }
66
- def self.[] (n)
66
+ def self.[](n)
67
67
  return N_TO_CLASS[n] || self
68
68
  end
69
69
  end # class ResultError