net-ldap 0.3.1 → 0.17.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.
- checksums.yaml +7 -0
- data/Contributors.rdoc +4 -0
- data/Hacking.rdoc +3 -8
- data/History.rdoc +181 -0
- data/README.rdoc +44 -12
- data/lib/net-ldap.rb +1 -1
- data/lib/net/ber.rb +41 -7
- data/lib/net/ber/ber_parser.rb +21 -7
- data/lib/net/ber/core_ext.rb +11 -18
- data/lib/net/ber/core_ext/array.rb +14 -0
- data/lib/net/ber/core_ext/integer.rb +74 -0
- data/lib/net/ber/core_ext/string.rb +24 -4
- data/lib/net/ber/core_ext/true_class.rb +2 -3
- data/lib/net/ldap.rb +441 -639
- data/lib/net/ldap/auth_adapter.rb +29 -0
- data/lib/net/ldap/auth_adapter/gss_spnego.rb +41 -0
- data/lib/net/ldap/auth_adapter/sasl.rb +62 -0
- data/lib/net/ldap/auth_adapter/simple.rb +34 -0
- data/lib/net/ldap/connection.rb +716 -0
- data/lib/net/ldap/dataset.rb +23 -9
- data/lib/net/ldap/dn.rb +13 -14
- data/lib/net/ldap/entry.rb +27 -9
- data/lib/net/ldap/error.rb +49 -0
- data/lib/net/ldap/filter.rb +58 -32
- data/lib/net/ldap/instrumentation.rb +23 -0
- data/lib/net/ldap/password.rb +23 -14
- data/lib/net/ldap/pdu.rb +70 -6
- data/lib/net/ldap/version.rb +5 -0
- data/lib/net/snmp.rb +237 -241
- metadata +71 -116
- data/.autotest +0 -11
- data/.gemtest +0 -0
- data/.rspec +0 -2
- data/Manifest.txt +0 -49
- data/Rakefile +0 -74
- data/autotest/discover.rb +0 -1
- data/lib/net/ber/core_ext/bignum.rb +0 -22
- data/lib/net/ber/core_ext/fixnum.rb +0 -66
- data/net-ldap.gemspec +0 -58
- data/spec/integration/ssl_ber_spec.rb +0 -36
- data/spec/spec.opts +0 -2
- data/spec/spec_helper.rb +0 -5
- data/spec/unit/ber/ber_spec.rb +0 -109
- data/spec/unit/ber/core_ext/string_spec.rb +0 -51
- data/spec/unit/ldap/dn_spec.rb +0 -80
- data/spec/unit/ldap/entry_spec.rb +0 -51
- data/spec/unit/ldap/filter_spec.rb +0 -84
- data/spec/unit/ldap_spec.rb +0 -48
- data/test/common.rb +0 -3
- data/test/test_entry.rb +0 -59
- data/test/test_filter.rb +0 -122
- data/test/test_ldap_connection.rb +0 -24
- data/test/test_ldif.rb +0 -79
- data/test/test_password.rb +0 -17
- data/test/test_rename.rb +0 -77
- data/test/test_snmp.rb +0 -114
- data/test/testdata.ldif +0 -101
- data/testserver/ldapserver.rb +0 -210
- data/testserver/testdata.ldif +0 -101
@@ -0,0 +1,29 @@
|
|
1
|
+
module Net
|
2
|
+
class LDAP
|
3
|
+
class AuthAdapter
|
4
|
+
def self.register(names, adapter)
|
5
|
+
names = Array(names)
|
6
|
+
@adapters ||= {}
|
7
|
+
names.each do |name|
|
8
|
+
@adapters[name] = adapter
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.[](name)
|
13
|
+
a = @adapters[name]
|
14
|
+
if a.nil?
|
15
|
+
raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{name})"
|
16
|
+
end
|
17
|
+
return a
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(conn)
|
21
|
+
@connection = conn
|
22
|
+
end
|
23
|
+
|
24
|
+
def bind
|
25
|
+
raise "bind method must be overwritten"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../auth_adapter'
|
2
|
+
require_relative 'sasl'
|
3
|
+
|
4
|
+
module Net
|
5
|
+
class LDAP
|
6
|
+
module AuthAdapers
|
7
|
+
#--
|
8
|
+
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
|
9
|
+
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
|
10
|
+
# integrate it without introducing an external dependency.
|
11
|
+
#
|
12
|
+
# This authentication method is accessed by calling #bind with a :method
|
13
|
+
# parameter of :gss_spnego. It requires :username and :password
|
14
|
+
# attributes, just like the :simple authentication method. It performs a
|
15
|
+
# GSS-SPNEGO authentication with the server, which is presumed to be a
|
16
|
+
# Microsoft Active Directory.
|
17
|
+
#++
|
18
|
+
class GSS_SPNEGO < Net::LDAP::AuthAdapter
|
19
|
+
def bind(auth)
|
20
|
+
require 'ntlm'
|
21
|
+
|
22
|
+
user, psw = [auth[:username] || auth[:dn], auth[:password]]
|
23
|
+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
|
24
|
+
|
25
|
+
nego = proc do |challenge|
|
26
|
+
t2_msg = NTLM::Message.parse(challenge)
|
27
|
+
t3_msg = t2_msg.response({ :user => user, :password => psw },
|
28
|
+
{ :ntlmv2 => true })
|
29
|
+
t3_msg.serialize
|
30
|
+
end
|
31
|
+
|
32
|
+
Net::LDAP::AuthAdapter::Sasl.new(@connection).bind \
|
33
|
+
:method => :sasl,
|
34
|
+
:mechanism => "GSS-SPNEGO",
|
35
|
+
:initial_credential => NTLM::Message::Type1.new.serialize,
|
36
|
+
:challenge_response => nego
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative '../auth_adapter'
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class LDAP
|
5
|
+
class AuthAdapter
|
6
|
+
class Sasl < Net::LDAP::AuthAdapter
|
7
|
+
MAX_SASL_CHALLENGES = 10
|
8
|
+
|
9
|
+
#--
|
10
|
+
# Required parameters: :mechanism, :initial_credential and
|
11
|
+
# :challenge_response
|
12
|
+
#
|
13
|
+
# Mechanism is a string value that will be passed in the SASL-packet's
|
14
|
+
# "mechanism" field.
|
15
|
+
#
|
16
|
+
# Initial credential is most likely a string. It's passed in the initial
|
17
|
+
# BindRequest that goes to the server. In some protocols, it may be empty.
|
18
|
+
#
|
19
|
+
# Challenge-response is a Ruby proc that takes a single parameter and
|
20
|
+
# returns an object that will typically be a string. The
|
21
|
+
# challenge-response block is called when the server returns a
|
22
|
+
# BindResponse with a result code of 14 (saslBindInProgress). The
|
23
|
+
# challenge-response block receives a parameter containing the data
|
24
|
+
# returned by the server in the saslServerCreds field of the LDAP
|
25
|
+
# BindResponse packet. The challenge-response block may be called multiple
|
26
|
+
# times during the course of a SASL authentication, and each time it must
|
27
|
+
# return a value that will be passed back to the server as the credential
|
28
|
+
# data in the next BindRequest packet.
|
29
|
+
#++
|
30
|
+
def bind(auth)
|
31
|
+
mech, cred, chall = auth[:mechanism], auth[:initial_credential],
|
32
|
+
auth[:challenge_response]
|
33
|
+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)
|
34
|
+
|
35
|
+
message_id = @connection.next_msgid
|
36
|
+
|
37
|
+
n = 0
|
38
|
+
loop do
|
39
|
+
sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
|
40
|
+
request = [
|
41
|
+
Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl
|
42
|
+
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
|
43
|
+
|
44
|
+
@connection.send(:write, request, nil, message_id)
|
45
|
+
pdu = @connection.queued_read(message_id)
|
46
|
+
|
47
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
|
48
|
+
raise Net::LDAP::NoBindResultError, "no bind result"
|
49
|
+
end
|
50
|
+
|
51
|
+
return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
|
52
|
+
raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MAX_SASL_CHALLENGES)
|
53
|
+
|
54
|
+
cred = chall.call(pdu.result_server_sasl_creds)
|
55
|
+
end
|
56
|
+
|
57
|
+
raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative '../auth_adapter'
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class LDAP
|
5
|
+
class AuthAdapter
|
6
|
+
class Simple < AuthAdapter
|
7
|
+
def bind(auth)
|
8
|
+
user, psw = if auth[:method] == :simple
|
9
|
+
[auth[:username] || auth[:dn], auth[:password]]
|
10
|
+
else
|
11
|
+
["", ""]
|
12
|
+
end
|
13
|
+
|
14
|
+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
|
15
|
+
|
16
|
+
message_id = @connection.next_msgid
|
17
|
+
request = [
|
18
|
+
Net::LDAP::Connection::LdapVersion.to_ber, user.to_ber,
|
19
|
+
psw.to_ber_contextspecific(0)
|
20
|
+
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
|
21
|
+
|
22
|
+
@connection.send(:write, request, nil, message_id)
|
23
|
+
pdu = @connection.queued_read(message_id)
|
24
|
+
|
25
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
|
26
|
+
raise Net::LDAP::NoBindResultError, "no bind result"
|
27
|
+
end
|
28
|
+
|
29
|
+
pdu
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,716 @@
|
|
1
|
+
# This is a private class used internally by the library. It should not
|
2
|
+
# be called by user code.
|
3
|
+
class Net::LDAP::Connection #:nodoc:
|
4
|
+
include Net::LDAP::Instrumentation
|
5
|
+
|
6
|
+
# Seconds before failing for socket connect timeout
|
7
|
+
DefaultConnectTimeout = 5
|
8
|
+
|
9
|
+
LdapVersion = 3
|
10
|
+
|
11
|
+
# Initialize a connection to an LDAP server
|
12
|
+
#
|
13
|
+
# :server
|
14
|
+
# :hosts Array of tuples specifying host, port
|
15
|
+
# :host host
|
16
|
+
# :port port
|
17
|
+
# :socket prepared socket
|
18
|
+
#
|
19
|
+
def initialize(server = {})
|
20
|
+
@server = server
|
21
|
+
@instrumentation_service = server[:instrumentation_service]
|
22
|
+
|
23
|
+
# Allows tests to parameterize what socket class to use
|
24
|
+
@socket_class = server.fetch(:socket_class, DefaultSocket)
|
25
|
+
|
26
|
+
yield self if block_given?
|
27
|
+
end
|
28
|
+
|
29
|
+
def socket_class=(socket_class)
|
30
|
+
@socket_class = socket_class
|
31
|
+
end
|
32
|
+
|
33
|
+
def prepare_socket(server, timeout=nil)
|
34
|
+
socket = server[:socket]
|
35
|
+
encryption = server[:encryption]
|
36
|
+
|
37
|
+
@conn = socket
|
38
|
+
setup_encryption(encryption, timeout) if encryption
|
39
|
+
end
|
40
|
+
|
41
|
+
def open_connection(server)
|
42
|
+
hosts = server[:hosts]
|
43
|
+
encryption = server[:encryption]
|
44
|
+
|
45
|
+
timeout = server[:connect_timeout] || DefaultConnectTimeout
|
46
|
+
socket_opts = {
|
47
|
+
connect_timeout: timeout,
|
48
|
+
}
|
49
|
+
|
50
|
+
errors = []
|
51
|
+
hosts.each do |host, port|
|
52
|
+
begin
|
53
|
+
prepare_socket(server.merge(socket: @socket_class.new(host, port, socket_opts)), timeout)
|
54
|
+
if encryption
|
55
|
+
if encryption[:tls_options] &&
|
56
|
+
encryption[:tls_options][:verify_mode] &&
|
57
|
+
encryption[:tls_options][:verify_mode] == OpenSSL::SSL::VERIFY_NONE
|
58
|
+
warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'"
|
59
|
+
else
|
60
|
+
@conn.post_connection_check(host)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return
|
64
|
+
rescue Net::LDAP::Error, SocketError, SystemCallError,
|
65
|
+
OpenSSL::SSL::SSLError => e
|
66
|
+
# Ensure the connection is closed in the event a setup failure.
|
67
|
+
close
|
68
|
+
errors << [e, host, port]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
raise Net::LDAP::ConnectionError.new(errors)
|
73
|
+
end
|
74
|
+
|
75
|
+
module GetbyteForSSLSocket
|
76
|
+
def getbyte
|
77
|
+
getc.ord
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module FixSSLSocketSyncClose
|
82
|
+
def close
|
83
|
+
super
|
84
|
+
io.close
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.wrap_with_ssl(io, tls_options = {}, timeout=nil)
|
89
|
+
raise Net::LDAP::NoOpenSSLError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
|
90
|
+
|
91
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
92
|
+
|
93
|
+
# By default, we do not verify certificates. For a 1.0 release, this should probably be changed at some point.
|
94
|
+
# See discussion in https://github.com/ruby-ldap/ruby-net-ldap/pull/161
|
95
|
+
ctx.set_params(tls_options) unless tls_options.empty?
|
96
|
+
|
97
|
+
conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
|
98
|
+
|
99
|
+
begin
|
100
|
+
if timeout
|
101
|
+
conn.connect_nonblock
|
102
|
+
else
|
103
|
+
conn.connect
|
104
|
+
end
|
105
|
+
rescue IO::WaitReadable
|
106
|
+
raise Errno::ETIMEDOUT, "OpenSSL connection read timeout" unless
|
107
|
+
IO.select([conn], nil, nil, timeout)
|
108
|
+
retry
|
109
|
+
rescue IO::WaitWritable
|
110
|
+
raise Errno::ETIMEDOUT, "OpenSSL connection write timeout" unless
|
111
|
+
IO.select(nil, [conn], nil, timeout)
|
112
|
+
retry
|
113
|
+
end
|
114
|
+
|
115
|
+
# Doesn't work:
|
116
|
+
# conn.sync_close = true
|
117
|
+
|
118
|
+
conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
|
119
|
+
conn.extend(FixSSLSocketSyncClose)
|
120
|
+
|
121
|
+
conn
|
122
|
+
end
|
123
|
+
|
124
|
+
#--
|
125
|
+
# Helper method called only from prepare_socket or open_connection, and only
|
126
|
+
# after we have a successfully-opened @conn instance variable, which is a TCP
|
127
|
+
# connection. Depending on the received arguments, we establish SSL,
|
128
|
+
# potentially replacing the value of @conn accordingly. Don't generate any
|
129
|
+
# errors here if no encryption is requested. DO raise Net::LDAP::Error objects
|
130
|
+
# if encryption is requested and we have trouble setting it up. That includes
|
131
|
+
# if OpenSSL is not set up on the machine. (Question: how does the Ruby
|
132
|
+
# OpenSSL wrapper react in that case?) DO NOT filter exceptions raised by the
|
133
|
+
# OpenSSL library. Let them pass back to the user. That should make it easier
|
134
|
+
# for us to debug the problem reports. Presumably (hopefully?) that will also
|
135
|
+
# produce recognizable errors if someone tries to use this on a machine
|
136
|
+
# without OpenSSL.
|
137
|
+
#
|
138
|
+
# The simple_tls method is intended as the simplest, stupidest, easiest
|
139
|
+
# solution for people who want nothing more than encrypted comms with the
|
140
|
+
# LDAP server. It doesn't do any server-cert validation and requires
|
141
|
+
# nothing in the way of key files and root-cert files, etc etc. OBSERVE:
|
142
|
+
# WE REPLACE the value of @conn, which is presumed to be a connected
|
143
|
+
# TCPSocket object.
|
144
|
+
#
|
145
|
+
# The start_tls method is supported by many servers over the standard LDAP
|
146
|
+
# port. It does not require an alternative port for encrypted
|
147
|
+
# communications, as with simple_tls. Thanks for Kouhei Sutou for
|
148
|
+
# generously contributing the :start_tls path.
|
149
|
+
#++
|
150
|
+
def setup_encryption(args, timeout=nil)
|
151
|
+
args[:tls_options] ||= {}
|
152
|
+
case args[:method]
|
153
|
+
when :simple_tls
|
154
|
+
@conn = self.class.wrap_with_ssl(@conn, args[:tls_options], timeout)
|
155
|
+
# additional branches requiring server validation and peer certs, etc.
|
156
|
+
# go here.
|
157
|
+
when :start_tls
|
158
|
+
message_id = next_msgid
|
159
|
+
request = [
|
160
|
+
Net::LDAP::StartTlsOid.to_ber_contextspecific(0),
|
161
|
+
].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
|
162
|
+
|
163
|
+
write(request, nil, message_id)
|
164
|
+
pdu = queued_read(message_id)
|
165
|
+
|
166
|
+
if pdu.nil? || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
|
167
|
+
raise Net::LDAP::NoStartTLSResultError, "no start_tls result"
|
168
|
+
end
|
169
|
+
|
170
|
+
raise Net::LDAP::StartTLSError,
|
171
|
+
"start_tls failed: #{pdu.result_code}" unless pdu.result_code.zero?
|
172
|
+
@conn = self.class.wrap_with_ssl(@conn, args[:tls_options], timeout)
|
173
|
+
else
|
174
|
+
raise Net::LDAP::EncMethodUnsupportedError, "unsupported encryption method #{args[:method]}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
#--
|
179
|
+
# This is provided as a convenience method to make sure a connection
|
180
|
+
# object gets closed without waiting for a GC to happen. Clients shouldn't
|
181
|
+
# have to call it, but perhaps it will come in handy someday.
|
182
|
+
#++
|
183
|
+
def close
|
184
|
+
return if !defined?(@conn) || @conn.nil?
|
185
|
+
@conn.close
|
186
|
+
@conn = nil
|
187
|
+
end
|
188
|
+
|
189
|
+
# Internal: Reads messages by ID from a queue, falling back to reading from
|
190
|
+
# the connected socket until a message matching the ID is read. Any messages
|
191
|
+
# with mismatched IDs gets queued for subsequent reads by the origin of that
|
192
|
+
# message ID.
|
193
|
+
#
|
194
|
+
# Returns a Net::LDAP::PDU object or nil.
|
195
|
+
def queued_read(message_id)
|
196
|
+
if pdu = message_queue[message_id].shift
|
197
|
+
return pdu
|
198
|
+
end
|
199
|
+
|
200
|
+
# read messages until we have a match for the given message_id
|
201
|
+
while pdu = read
|
202
|
+
return pdu if pdu.message_id == message_id
|
203
|
+
|
204
|
+
message_queue[pdu.message_id].push pdu
|
205
|
+
next
|
206
|
+
end
|
207
|
+
|
208
|
+
pdu
|
209
|
+
end
|
210
|
+
|
211
|
+
# Internal: The internal queue of messages, read from the socket, grouped by
|
212
|
+
# message ID.
|
213
|
+
#
|
214
|
+
# Used by `queued_read` to return messages sent by the server with the given
|
215
|
+
# ID. If no messages are queued for that ID, `queued_read` will `read` from
|
216
|
+
# the socket and queue messages that don't match the given ID for other
|
217
|
+
# readers.
|
218
|
+
#
|
219
|
+
# Returns the message queue Hash.
|
220
|
+
def message_queue
|
221
|
+
@message_queue ||= Hash.new do |hash, key|
|
222
|
+
hash[key] = []
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Internal: Reads and parses data from the configured connection.
|
227
|
+
#
|
228
|
+
# - syntax: the BER syntax to use to parse the read data with
|
229
|
+
#
|
230
|
+
# Returns parsed Net::LDAP::PDU object.
|
231
|
+
def read(syntax = Net::LDAP::AsnSyntax)
|
232
|
+
ber_object =
|
233
|
+
instrument "read.net_ldap_connection", :syntax => syntax do |payload|
|
234
|
+
socket.read_ber(syntax) do |id, content_length|
|
235
|
+
payload[:object_type_id] = id
|
236
|
+
payload[:content_length] = content_length
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
return unless ber_object
|
241
|
+
|
242
|
+
instrument "parse_pdu.net_ldap_connection" do |payload|
|
243
|
+
pdu = payload[:pdu] = Net::LDAP::PDU.new(ber_object)
|
244
|
+
|
245
|
+
payload[:message_id] = pdu.message_id
|
246
|
+
payload[:app_tag] = pdu.app_tag
|
247
|
+
|
248
|
+
pdu
|
249
|
+
end
|
250
|
+
end
|
251
|
+
private :read
|
252
|
+
|
253
|
+
# Internal: Write a BER formatted packet with the next message id to the
|
254
|
+
# configured connection.
|
255
|
+
#
|
256
|
+
# - request: required BER formatted request
|
257
|
+
# - controls: optional BER formatted controls
|
258
|
+
#
|
259
|
+
# Returns the return value from writing to the connection, which in some
|
260
|
+
# cases is the Integer number of bytes written to the socket.
|
261
|
+
def write(request, controls = nil, message_id = next_msgid)
|
262
|
+
instrument "write.net_ldap_connection" do |payload|
|
263
|
+
packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
|
264
|
+
payload[:content_length] = socket.write(packet)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
private :write
|
268
|
+
|
269
|
+
def next_msgid
|
270
|
+
@msgid ||= 0
|
271
|
+
@msgid += 1
|
272
|
+
end
|
273
|
+
|
274
|
+
def bind(auth)
|
275
|
+
instrument "bind.net_ldap_connection" do |payload|
|
276
|
+
payload[:method] = meth = auth[:method]
|
277
|
+
adapter = Net::LDAP::AuthAdapter[meth]
|
278
|
+
adapter.new(self).bind(auth)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
#--
|
283
|
+
# Allow the caller to specify a sort control
|
284
|
+
#
|
285
|
+
# The format of the sort control needs to be:
|
286
|
+
#
|
287
|
+
# :sort_control => ["cn"] # just a string
|
288
|
+
# or
|
289
|
+
# :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
|
290
|
+
# or
|
291
|
+
# :sort_control => ["givenname","sn"] #multiple strings or arrays
|
292
|
+
#
|
293
|
+
def encode_sort_controls(sort_definitions)
|
294
|
+
return sort_definitions unless sort_definitions
|
295
|
+
|
296
|
+
sort_control_values = sort_definitions.map do |control|
|
297
|
+
control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
|
298
|
+
control[0] = String(control[0]).to_ber,
|
299
|
+
control[1] = String(control[1]).to_ber,
|
300
|
+
control[2] = (control[2] == true).to_ber
|
301
|
+
control.to_ber_sequence
|
302
|
+
end
|
303
|
+
[
|
304
|
+
Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
|
305
|
+
false.to_ber,
|
306
|
+
sort_control_values.to_ber_sequence.to_s.to_ber,
|
307
|
+
].to_ber_sequence
|
308
|
+
end
|
309
|
+
|
310
|
+
#--
|
311
|
+
# Alternate implementation, this yields each search entry to the caller as
|
312
|
+
# it are received.
|
313
|
+
#
|
314
|
+
# TODO: certain search parameters are hardcoded.
|
315
|
+
# TODO: if we mis-parse the server results or the results are wrong, we
|
316
|
+
# can block forever. That's because we keep reading results until we get a
|
317
|
+
# type-5 packet, which might never come. We need to support the time-limit
|
318
|
+
# in the protocol.
|
319
|
+
#++
|
320
|
+
def search(args = nil)
|
321
|
+
args ||= {}
|
322
|
+
|
323
|
+
# filtering, scoping, search base
|
324
|
+
# filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
|
325
|
+
# base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
|
326
|
+
# scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2
|
327
|
+
filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")
|
328
|
+
base = args[:base]
|
329
|
+
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
|
330
|
+
|
331
|
+
# attr handling
|
332
|
+
# attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8
|
333
|
+
# attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
|
334
|
+
attrs = Array(args[:attributes])
|
335
|
+
attrs_only = args[:attributes_only] == true
|
336
|
+
|
337
|
+
# references
|
338
|
+
# refs: https://tools.ietf.org/html/rfc4511#section-4.5.3
|
339
|
+
# deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
|
340
|
+
refs = args[:return_referrals] == true
|
341
|
+
deref = args[:deref] || Net::LDAP::DerefAliases_Never
|
342
|
+
|
343
|
+
# limiting, paging, sorting
|
344
|
+
# size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
|
345
|
+
# time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
|
346
|
+
size = args[:size].to_i
|
347
|
+
time = args[:time].to_i
|
348
|
+
paged = args[:paged_searches_supported]
|
349
|
+
sort = args.fetch(:sort_controls, false)
|
350
|
+
|
351
|
+
# arg validation
|
352
|
+
raise ArgumentError, "search base is required" unless base
|
353
|
+
raise ArgumentError, "invalid search-size" unless size >= 0
|
354
|
+
raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
|
355
|
+
raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)
|
356
|
+
|
357
|
+
# arg transforms
|
358
|
+
filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)
|
359
|
+
ber_attrs = attrs.map { |attr| attr.to_s.to_ber }
|
360
|
+
ber_sort = encode_sort_controls(sort)
|
361
|
+
|
362
|
+
# An interesting value for the size limit would be close to A/D's
|
363
|
+
# built-in page limit of 1000 records, but openLDAP newer than version
|
364
|
+
# 2.2.0 chokes on anything bigger than 126. You get a silent error that
|
365
|
+
# is easily visible by running slapd in debug mode. Go figure.
|
366
|
+
#
|
367
|
+
# Changed this around 06Sep06 to support a caller-specified search-size
|
368
|
+
# limit. Because we ALWAYS do paged searches, we have to work around the
|
369
|
+
# problem that it's not legal to specify a "normal" sizelimit (in the
|
370
|
+
# body of the search request) that is larger than the page size we're
|
371
|
+
# requesting. Unfortunately, I have the feeling that this will break
|
372
|
+
# with LDAP servers that don't support paged searches!!!
|
373
|
+
#
|
374
|
+
# (Because we pass zero as the sizelimit on search rounds when the
|
375
|
+
# remaining limit is larger than our max page size of 126. In these
|
376
|
+
# cases, I think the caller's search limit will be ignored!)
|
377
|
+
#
|
378
|
+
# CONFIRMED: This code doesn't work on LDAPs that don't support paged
|
379
|
+
# searches when the size limit is larger than 126. We're going to have
|
380
|
+
# to do a root-DSE record search and not do a paged search if the LDAP
|
381
|
+
# doesn't support it. Yuck.
|
382
|
+
rfc2696_cookie = [126, ""]
|
383
|
+
result_pdu = nil
|
384
|
+
n_results = 0
|
385
|
+
|
386
|
+
message_id = next_msgid
|
387
|
+
|
388
|
+
instrument "search.net_ldap_connection",
|
389
|
+
message_id: message_id,
|
390
|
+
filter: filter,
|
391
|
+
base: base,
|
392
|
+
scope: scope,
|
393
|
+
size: size,
|
394
|
+
time: time,
|
395
|
+
sort: sort,
|
396
|
+
referrals: refs,
|
397
|
+
deref: deref,
|
398
|
+
attributes: attrs do |payload|
|
399
|
+
loop do
|
400
|
+
# should collect this into a private helper to clarify the structure
|
401
|
+
query_limit = 0
|
402
|
+
if size > 0
|
403
|
+
query_limit = if paged
|
404
|
+
(((size - n_results) < 126) ? (size - n_results) : 0)
|
405
|
+
else
|
406
|
+
size
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
request = [
|
411
|
+
base.to_ber,
|
412
|
+
scope.to_ber_enumerated,
|
413
|
+
deref.to_ber_enumerated,
|
414
|
+
query_limit.to_ber, # size limit
|
415
|
+
time.to_ber,
|
416
|
+
attrs_only.to_ber,
|
417
|
+
filter.to_ber,
|
418
|
+
ber_attrs.to_ber_sequence,
|
419
|
+
].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)
|
420
|
+
|
421
|
+
# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
|
422
|
+
# this breaks when calling to_ber. (Can't force binary data to UTF-8)
|
423
|
+
# we have to disable paging (even though server supports it) to get around this...
|
424
|
+
|
425
|
+
controls = []
|
426
|
+
controls <<
|
427
|
+
[
|
428
|
+
Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
|
429
|
+
# Criticality MUST be false to interoperate with normal LDAPs.
|
430
|
+
false.to_ber,
|
431
|
+
rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber,
|
432
|
+
].to_ber_sequence if paged
|
433
|
+
controls << ber_sort if ber_sort
|
434
|
+
controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
|
435
|
+
|
436
|
+
write(request, controls, message_id)
|
437
|
+
|
438
|
+
result_pdu = nil
|
439
|
+
controls = []
|
440
|
+
|
441
|
+
while pdu = queued_read(message_id)
|
442
|
+
case pdu.app_tag
|
443
|
+
when Net::LDAP::PDU::SearchReturnedData
|
444
|
+
n_results += 1
|
445
|
+
yield pdu.search_entry if block_given?
|
446
|
+
when Net::LDAP::PDU::SearchResultReferral
|
447
|
+
if refs
|
448
|
+
if block_given?
|
449
|
+
se = Net::LDAP::Entry.new
|
450
|
+
se[:search_referrals] = (pdu.search_referrals || [])
|
451
|
+
yield se
|
452
|
+
end
|
453
|
+
end
|
454
|
+
when Net::LDAP::PDU::SearchResult
|
455
|
+
result_pdu = pdu
|
456
|
+
controls = pdu.result_controls
|
457
|
+
if refs && pdu.result_code == Net::LDAP::ResultCodeReferral
|
458
|
+
if block_given?
|
459
|
+
se = Net::LDAP::Entry.new
|
460
|
+
se[:search_referrals] = (pdu.search_referrals || [])
|
461
|
+
yield se
|
462
|
+
end
|
463
|
+
end
|
464
|
+
break
|
465
|
+
else
|
466
|
+
raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
if result_pdu.nil?
|
471
|
+
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing"
|
472
|
+
end
|
473
|
+
|
474
|
+
# count number of pages of results
|
475
|
+
payload[:page_count] ||= 0
|
476
|
+
payload[:page_count] += 1
|
477
|
+
|
478
|
+
# When we get here, we have seen a type-5 response. If there is no
|
479
|
+
# error AND there is an RFC-2696 cookie, then query again for the next
|
480
|
+
# page of results. If not, we're done. Don't screw this up or we'll
|
481
|
+
# break every search we do.
|
482
|
+
#
|
483
|
+
# Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
|
484
|
+
# that have a parameter of AsnSyntax? Does this just accidentally
|
485
|
+
# work? According to RFC-2696, the value expected in this position is
|
486
|
+
# of type OCTET STRING, covered in the default syntax supported by
|
487
|
+
# read_ber, so I guess we're ok.
|
488
|
+
more_pages = false
|
489
|
+
if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls
|
490
|
+
controls.each do |c|
|
491
|
+
if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
|
492
|
+
# just in case some bogus server sends us more than 1 of these.
|
493
|
+
more_pages = false
|
494
|
+
if c.value and c.value.length > 0
|
495
|
+
cookie = c.value.read_ber[1]
|
496
|
+
if cookie and cookie.length > 0
|
497
|
+
rfc2696_cookie[1] = cookie
|
498
|
+
more_pages = true
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
break unless more_pages
|
506
|
+
end # loop
|
507
|
+
|
508
|
+
# track total result count
|
509
|
+
payload[:result_count] = n_results
|
510
|
+
|
511
|
+
result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")
|
512
|
+
end # instrument
|
513
|
+
ensure
|
514
|
+
|
515
|
+
# clean up message queue for this search
|
516
|
+
messages = message_queue.delete(message_id)
|
517
|
+
|
518
|
+
# in the exceptional case some messages were *not* consumed from the queue,
|
519
|
+
# instrument the event but do not fail.
|
520
|
+
if !messages.nil? && !messages.empty?
|
521
|
+
instrument "search_messages_unread.net_ldap_connection",
|
522
|
+
message_id: message_id, messages: messages
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
MODIFY_OPERATIONS = { #:nodoc:
|
527
|
+
:add => 0,
|
528
|
+
:delete => 1,
|
529
|
+
:replace => 2,
|
530
|
+
}
|
531
|
+
|
532
|
+
def self.modify_ops(operations)
|
533
|
+
ops = []
|
534
|
+
if operations
|
535
|
+
operations.each do |op, attrib, values|
|
536
|
+
# TODO, fix the following line, which gives a bogus error if the
|
537
|
+
# opcode is invalid.
|
538
|
+
op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
|
539
|
+
values = [values].flatten.map { |v| v.to_ber if v }.to_ber_set
|
540
|
+
values = [attrib.to_s.to_ber, values].to_ber_sequence
|
541
|
+
ops << [op_ber, values].to_ber
|
542
|
+
end
|
543
|
+
end
|
544
|
+
ops
|
545
|
+
end
|
546
|
+
|
547
|
+
#--
|
548
|
+
# TODO: need to support a time limit, in case the server fails to respond.
|
549
|
+
# TODO: We're throwing an exception here on empty DN. Should return a
|
550
|
+
# proper error instead, probaby from farther up the chain.
|
551
|
+
# TODO: If the user specifies a bogus opcode, we'll throw a confusing
|
552
|
+
# error here ("to_ber_enumerated is not defined on nil").
|
553
|
+
#++
|
554
|
+
def modify(args)
|
555
|
+
modify_dn = args[:dn] or raise "Unable to modify empty DN"
|
556
|
+
ops = self.class.modify_ops args[:operations]
|
557
|
+
|
558
|
+
message_id = next_msgid
|
559
|
+
request = [
|
560
|
+
modify_dn.to_ber,
|
561
|
+
ops.to_ber_sequence,
|
562
|
+
].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)
|
563
|
+
|
564
|
+
write(request, nil, message_id)
|
565
|
+
pdu = queued_read(message_id)
|
566
|
+
|
567
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse
|
568
|
+
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
|
569
|
+
end
|
570
|
+
|
571
|
+
pdu
|
572
|
+
end
|
573
|
+
|
574
|
+
##
|
575
|
+
# Password Modify
|
576
|
+
#
|
577
|
+
# http://tools.ietf.org/html/rfc3062
|
578
|
+
#
|
579
|
+
# passwdModifyOID OBJECT IDENTIFIER ::= 1.3.6.1.4.1.4203.1.11.1
|
580
|
+
#
|
581
|
+
# PasswdModifyRequestValue ::= SEQUENCE {
|
582
|
+
# userIdentity [0] OCTET STRING OPTIONAL
|
583
|
+
# oldPasswd [1] OCTET STRING OPTIONAL
|
584
|
+
# newPasswd [2] OCTET STRING OPTIONAL }
|
585
|
+
#
|
586
|
+
# PasswdModifyResponseValue ::= SEQUENCE {
|
587
|
+
# genPasswd [0] OCTET STRING OPTIONAL }
|
588
|
+
#
|
589
|
+
# Encoded request:
|
590
|
+
#
|
591
|
+
# 00\x02\x01\x02w+\x80\x171.3.6.1.4.1.4203.1.11.1\x81\x100\x0E\x81\x05old\x82\x05new
|
592
|
+
#
|
593
|
+
def password_modify(args)
|
594
|
+
dn = args[:dn]
|
595
|
+
raise ArgumentError, 'DN is required' if !dn || dn.empty?
|
596
|
+
|
597
|
+
ext_seq = [Net::LDAP::PasswdModifyOid.to_ber_contextspecific(0)]
|
598
|
+
|
599
|
+
pwd_seq = []
|
600
|
+
pwd_seq << dn.to_ber(0x80)
|
601
|
+
pwd_seq << args[:old_password].to_ber(0x81) unless args[:old_password].nil?
|
602
|
+
pwd_seq << args[:new_password].to_ber(0x82) unless args[:new_password].nil?
|
603
|
+
ext_seq << pwd_seq.to_ber_sequence.to_ber(0x81)
|
604
|
+
|
605
|
+
request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
|
606
|
+
|
607
|
+
message_id = next_msgid
|
608
|
+
|
609
|
+
write(request, nil, message_id)
|
610
|
+
pdu = queued_read(message_id)
|
611
|
+
|
612
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
|
613
|
+
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
|
614
|
+
end
|
615
|
+
|
616
|
+
pdu
|
617
|
+
end
|
618
|
+
|
619
|
+
#--
|
620
|
+
# TODO: need to support a time limit, in case the server fails to respond.
|
621
|
+
# Unlike other operation-methods in this class, we return a result hash
|
622
|
+
# rather than a simple result number. This is experimental, and eventually
|
623
|
+
# we'll want to do this with all the others. The point is to have access
|
624
|
+
# to the error message and the matched-DN returned by the server.
|
625
|
+
#++
|
626
|
+
def add(args)
|
627
|
+
add_dn = args[:dn] or raise Net::LDAP::EmptyDNError, "Unable to add empty DN"
|
628
|
+
add_attrs = []
|
629
|
+
a = args[:attributes] and a.each do |k, v|
|
630
|
+
add_attrs << [k.to_s.to_ber, Array(v).map(&:to_ber).to_ber_set].to_ber_sequence
|
631
|
+
end
|
632
|
+
|
633
|
+
message_id = next_msgid
|
634
|
+
request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest)
|
635
|
+
|
636
|
+
write(request, nil, message_id)
|
637
|
+
pdu = queued_read(message_id)
|
638
|
+
|
639
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse
|
640
|
+
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
|
641
|
+
end
|
642
|
+
|
643
|
+
pdu
|
644
|
+
end
|
645
|
+
|
646
|
+
#--
|
647
|
+
# TODO: need to support a time limit, in case the server fails to respond.
|
648
|
+
#++
|
649
|
+
def rename(args)
|
650
|
+
old_dn = args[:olddn] or raise "Unable to rename empty DN"
|
651
|
+
new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
|
652
|
+
delete_attrs = args[:delete_attributes] ? true : false
|
653
|
+
new_superior = args[:new_superior]
|
654
|
+
|
655
|
+
message_id = next_msgid
|
656
|
+
request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
|
657
|
+
request << new_superior.to_ber_contextspecific(0) unless new_superior == nil
|
658
|
+
|
659
|
+
write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest), nil, message_id)
|
660
|
+
pdu = queued_read(message_id)
|
661
|
+
|
662
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyRDNResponse
|
663
|
+
raise Net::LDAP::ResponseMissingOrInvalidError.new "response missing or invalid"
|
664
|
+
end
|
665
|
+
|
666
|
+
pdu
|
667
|
+
end
|
668
|
+
|
669
|
+
#--
|
670
|
+
# TODO, need to support a time limit, in case the server fails to respond.
|
671
|
+
#++
|
672
|
+
def delete(args)
|
673
|
+
dn = args[:dn] or raise "Unable to delete empty DN"
|
674
|
+
controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
|
675
|
+
message_id = next_msgid
|
676
|
+
request = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest)
|
677
|
+
|
678
|
+
write(request, controls, message_id)
|
679
|
+
pdu = queued_read(message_id)
|
680
|
+
|
681
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::DeleteResponse
|
682
|
+
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
|
683
|
+
end
|
684
|
+
|
685
|
+
pdu
|
686
|
+
end
|
687
|
+
|
688
|
+
# Internal: Returns a Socket like object used internally to communicate with
|
689
|
+
# LDAP server.
|
690
|
+
#
|
691
|
+
# Typically a TCPSocket, but can be a OpenSSL::SSL::SSLSocket
|
692
|
+
def socket
|
693
|
+
return @conn if defined?(@conn) && !@conn.nil?
|
694
|
+
|
695
|
+
# First refactoring uses the existing methods open_connection and
|
696
|
+
# prepare_socket to set @conn. Next cleanup would centralize connection
|
697
|
+
# handling here.
|
698
|
+
if @server[:socket]
|
699
|
+
prepare_socket(@server)
|
700
|
+
else
|
701
|
+
@server[:hosts] = [[@server[:host], @server[:port]]] if @server[:hosts].nil?
|
702
|
+
open_connection(@server)
|
703
|
+
end
|
704
|
+
|
705
|
+
@conn
|
706
|
+
end
|
707
|
+
|
708
|
+
private
|
709
|
+
|
710
|
+
# Wrap around Socket.tcp to normalize with other Socket initializers
|
711
|
+
class DefaultSocket
|
712
|
+
def self.new(host, port, socket_opts = {})
|
713
|
+
Socket.tcp(host, port, **socket_opts)
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end # class Connection
|