To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / .svn / pristine / c8 / c84d690d9bd0576dfc0e1af20e017bb13ff2e71d.svn-base @ 1297:0a574315af3e
History | View | Annotate | Download (53.1 KB)
| 1 |
# $Id: ldap.rb 154 2006-08-15 09:35:43Z blackhedd $ |
|---|---|
| 2 |
# |
| 3 |
# Net::LDAP for Ruby |
| 4 |
# |
| 5 |
# |
| 6 |
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. |
| 7 |
# |
| 8 |
# Written and maintained by Francis Cianfrocca, gmail: garbagecat10. |
| 9 |
# |
| 10 |
# This program is free software. |
| 11 |
# You may re-distribute and/or modify this program under the same terms |
| 12 |
# as Ruby itself: Ruby Distribution License or GNU General Public License. |
| 13 |
# |
| 14 |
# |
| 15 |
# See Net::LDAP for documentation and usage samples. |
| 16 |
# |
| 17 |
|
| 18 |
|
| 19 |
require 'socket' |
| 20 |
require 'ostruct' |
| 21 |
|
| 22 |
begin |
| 23 |
require 'openssl' |
| 24 |
$net_ldap_openssl_available = true |
| 25 |
rescue LoadError |
| 26 |
end |
| 27 |
|
| 28 |
require 'net/ber' |
| 29 |
require 'net/ldap/pdu' |
| 30 |
require 'net/ldap/filter' |
| 31 |
require 'net/ldap/dataset' |
| 32 |
require 'net/ldap/psw' |
| 33 |
require 'net/ldap/entry' |
| 34 |
|
| 35 |
|
| 36 |
module Net |
| 37 |
|
| 38 |
|
| 39 |
# == Net::LDAP |
| 40 |
# |
| 41 |
# This library provides a pure-Ruby implementation of the |
| 42 |
# LDAP client protocol, per RFC-2251. |
| 43 |
# It can be used to access any server which implements the |
| 44 |
# LDAP protocol. |
| 45 |
# |
| 46 |
# Net::LDAP is intended to provide full LDAP functionality |
| 47 |
# while hiding the more arcane aspects |
| 48 |
# the LDAP protocol itself, and thus presenting as Ruby-like |
| 49 |
# a programming interface as possible. |
| 50 |
# |
| 51 |
# == Quick-start for the Impatient |
| 52 |
# === Quick Example of a user-authentication against an LDAP directory: |
| 53 |
# |
| 54 |
# require 'rubygems' |
| 55 |
# require 'net/ldap' |
| 56 |
# |
| 57 |
# ldap = Net::LDAP.new |
| 58 |
# ldap.host = your_server_ip_address |
| 59 |
# ldap.port = 389 |
| 60 |
# ldap.auth "joe_user", "opensesame" |
| 61 |
# if ldap.bind |
| 62 |
# # authentication succeeded |
| 63 |
# else |
| 64 |
# # authentication failed |
| 65 |
# end |
| 66 |
# |
| 67 |
# |
| 68 |
# === Quick Example of a search against an LDAP directory: |
| 69 |
# |
| 70 |
# require 'rubygems' |
| 71 |
# require 'net/ldap' |
| 72 |
# |
| 73 |
# ldap = Net::LDAP.new :host => server_ip_address, |
| 74 |
# :port => 389, |
| 75 |
# :auth => {
|
| 76 |
# :method => :simple, |
| 77 |
# :username => "cn=manager,dc=example,dc=com", |
| 78 |
# :password => "opensesame" |
| 79 |
# } |
| 80 |
# |
| 81 |
# filter = Net::LDAP::Filter.eq( "cn", "George*" ) |
| 82 |
# treebase = "dc=example,dc=com" |
| 83 |
# |
| 84 |
# ldap.search( :base => treebase, :filter => filter ) do |entry| |
| 85 |
# puts "DN: #{entry.dn}"
|
| 86 |
# entry.each do |attribute, values| |
| 87 |
# puts " #{attribute}:"
|
| 88 |
# values.each do |value| |
| 89 |
# puts " --->#{value}"
|
| 90 |
# end |
| 91 |
# end |
| 92 |
# end |
| 93 |
# |
| 94 |
# p ldap.get_operation_result |
| 95 |
# |
| 96 |
# |
| 97 |
# == A Brief Introduction to LDAP |
| 98 |
# |
| 99 |
# We're going to provide a quick, informal introduction to LDAP |
| 100 |
# terminology and |
| 101 |
# typical operations. If you're comfortable with this material, skip |
| 102 |
# ahead to "How to use Net::LDAP." If you want a more rigorous treatment |
| 103 |
# of this material, we recommend you start with the various IETF and ITU |
| 104 |
# standards that relate to LDAP. |
| 105 |
# |
| 106 |
# === Entities |
| 107 |
# LDAP is an Internet-standard protocol used to access directory servers. |
| 108 |
# The basic search unit is the <i>entity,</i> which corresponds to |
| 109 |
# a person or other domain-specific object. |
| 110 |
# A directory service which supports the LDAP protocol typically |
| 111 |
# stores information about a number of entities. |
| 112 |
# |
| 113 |
# === Principals |
| 114 |
# LDAP servers are typically used to access information about people, |
| 115 |
# but also very often about such items as printers, computers, and other |
| 116 |
# resources. To reflect this, LDAP uses the term <i>entity,</i> or less |
| 117 |
# commonly, <i>principal,</i> to denote its basic data-storage unit. |
| 118 |
# |
| 119 |
# |
| 120 |
# === Distinguished Names |
| 121 |
# In LDAP's view of the world, |
| 122 |
# an entity is uniquely identified by a globally-unique text string |
| 123 |
# called a <i>Distinguished Name,</i> originally defined in the X.400 |
| 124 |
# standards from which LDAP is ultimately derived. |
| 125 |
# Much like a DNS hostname, a DN is a "flattened" text representation |
| 126 |
# of a string of tree nodes. Also like DNS (and unlike Java package |
| 127 |
# names), a DN expresses a chain of tree-nodes written from left to right |
| 128 |
# in order from the most-resolved node to the most-general one. |
| 129 |
# |
| 130 |
# If you know the DN of a person or other entity, then you can query |
| 131 |
# an LDAP-enabled directory for information (attributes) about the entity. |
| 132 |
# Alternatively, you can query the directory for a list of DNs matching |
| 133 |
# a set of criteria that you supply. |
| 134 |
# |
| 135 |
# === Attributes |
| 136 |
# |
| 137 |
# In the LDAP view of the world, a DN uniquely identifies an entity. |
| 138 |
# Information about the entity is stored as a set of <i>Attributes.</i> |
| 139 |
# An attribute is a text string which is associated with zero or more |
| 140 |
# values. Most LDAP-enabled directories store a well-standardized |
| 141 |
# range of attributes, and constrain their values according to standard |
| 142 |
# rules. |
| 143 |
# |
| 144 |
# A good example of an attribute is <tt>sn,</tt> which stands for "Surname." |
| 145 |
# This attribute is generally used to store a person's surname, or last name. |
| 146 |
# Most directories enforce the standard convention that |
| 147 |
# an entity's <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP |
| 148 |
# jargon, that means that <tt>sn</tt> must be <i>present</i> and |
| 149 |
# <i>single-valued.</i> |
| 150 |
# |
| 151 |
# Another attribute is <tt>mail,</tt> which is used to store email addresses. |
| 152 |
# (No, there is no attribute called "email," perhaps because X.400 terminology |
| 153 |
# predates the invention of the term <i>email.</i>) <tt>mail</tt> differs |
| 154 |
# from <tt>sn</tt> in that most directories permit any number of values for the |
| 155 |
# <tt>mail</tt> attribute, including zero. |
| 156 |
# |
| 157 |
# |
| 158 |
# === Tree-Base |
| 159 |
# We said above that X.400 Distinguished Names are <i>globally unique.</i> |
| 160 |
# In a manner reminiscent of DNS, LDAP supposes that each directory server |
| 161 |
# contains authoritative attribute data for a set of DNs corresponding |
| 162 |
# to a specific sub-tree of the (notional) global directory tree. |
| 163 |
# This subtree is generally configured into a directory server when it is |
| 164 |
# created. It matters for this discussion because most servers will not |
| 165 |
# allow you to query them unless you specify a correct tree-base. |
| 166 |
# |
| 167 |
# Let's say you work for the engineering department of Big Company, Inc., |
| 168 |
# whose internet domain is bigcompany.com. You may find that your departmental |
| 169 |
# directory is stored in a server with a defined tree-base of |
| 170 |
# ou=engineering,dc=bigcompany,dc=com |
| 171 |
# You will need to supply this string as the <i>tree-base</i> when querying this |
| 172 |
# directory. (Ou is a very old X.400 term meaning "organizational unit." |
| 173 |
# Dc is a more recent term meaning "domain component.") |
| 174 |
# |
| 175 |
# === LDAP Versions |
| 176 |
# (stub, discuss v2 and v3) |
| 177 |
# |
| 178 |
# === LDAP Operations |
| 179 |
# The essential operations are: #bind, #search, #add, #modify, #delete, and #rename. |
| 180 |
# ==== Bind |
| 181 |
# #bind supplies a user's authentication credentials to a server, which in turn verifies |
| 182 |
# or rejects them. There is a range of possibilities for credentials, but most directories |
| 183 |
# support a simple username and password authentication. |
| 184 |
# |
| 185 |
# Taken by itself, #bind can be used to authenticate a user against information |
| 186 |
# stored in a directory, for example to permit or deny access to some other resource. |
| 187 |
# In terms of the other LDAP operations, most directories require a successful #bind to |
| 188 |
# be performed before the other operations will be permitted. Some servers permit certain |
| 189 |
# operations to be performed with an "anonymous" binding, meaning that no credentials are |
| 190 |
# presented by the user. (We're glossing over a lot of platform-specific detail here.) |
| 191 |
# |
| 192 |
# ==== Search |
| 193 |
# Calling #search against the directory involves specifying a treebase, a set of <i>search filters,</i> |
| 194 |
# and a list of attribute values. |
| 195 |
# The filters specify ranges of possible values for particular attributes. Multiple |
| 196 |
# filters can be joined together with AND, OR, and NOT operators. |
| 197 |
# A server will respond to a #search by returning a list of matching DNs together with a |
| 198 |
# set of attribute values for each entity, depending on what attributes the search requested. |
| 199 |
# |
| 200 |
# ==== Add |
| 201 |
# #add specifies a new DN and an initial set of attribute values. If the operation |
| 202 |
# succeeds, a new entity with the corresponding DN and attributes is added to the directory. |
| 203 |
# |
| 204 |
# ==== Modify |
| 205 |
# #modify specifies an entity DN, and a list of attribute operations. #modify is used to change |
| 206 |
# the attribute values stored in the directory for a particular entity. |
| 207 |
# #modify may add or delete attributes (which are lists of values) or it change attributes by |
| 208 |
# adding to or deleting from their values. |
| 209 |
# Net::LDAP provides three easier methods to modify an entry's attribute values: |
| 210 |
# #add_attribute, #replace_attribute, and #delete_attribute. |
| 211 |
# |
| 212 |
# ==== Delete |
| 213 |
# #delete specifies an entity DN. If it succeeds, the entity and all its attributes |
| 214 |
# is removed from the directory. |
| 215 |
# |
| 216 |
# ==== Rename (or Modify RDN) |
| 217 |
# #rename (or #modify_rdn) is an operation added to version 3 of the LDAP protocol. It responds to |
| 218 |
# the often-arising need to change the DN of an entity without discarding its attribute values. |
| 219 |
# In earlier LDAP versions, the only way to do this was to delete the whole entity and add it |
| 220 |
# again with a different DN. |
| 221 |
# |
| 222 |
# #rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most |
| 223 |
# part of the DN string. If successful, #rename changes the entity DN so that its left-most |
| 224 |
# node corresponds to the new RDN given in the request. (RDN, or "relative distinguished name," |
| 225 |
# denotes a single tree-node as expressed in a DN, which is a chain of tree nodes.) |
| 226 |
# |
| 227 |
# == How to use Net::LDAP |
| 228 |
# |
| 229 |
# To access Net::LDAP functionality in your Ruby programs, start by requiring |
| 230 |
# the library: |
| 231 |
# |
| 232 |
# require 'net/ldap' |
| 233 |
# |
| 234 |
# If you installed the Gem version of Net::LDAP, and depending on your version of |
| 235 |
# Ruby and rubygems, you _may_ also need to require rubygems explicitly: |
| 236 |
# |
| 237 |
# require 'rubygems' |
| 238 |
# require 'net/ldap' |
| 239 |
# |
| 240 |
# Most operations with Net::LDAP start by instantiating a Net::LDAP object. |
| 241 |
# The constructor for this object takes arguments specifying the network location |
| 242 |
# (address and port) of the LDAP server, and also the binding (authentication) |
| 243 |
# credentials, typically a username and password. |
| 244 |
# Given an object of class Net:LDAP, you can then perform LDAP operations by calling |
| 245 |
# instance methods on the object. These are documented with usage examples below. |
| 246 |
# |
| 247 |
# The Net::LDAP library is designed to be very disciplined about how it makes network |
| 248 |
# connections to servers. This is different from many of the standard native-code |
| 249 |
# libraries that are provided on most platforms, which share bloodlines with the |
| 250 |
# original Netscape/Michigan LDAP client implementations. These libraries sought to |
| 251 |
# insulate user code from the workings of the network. This is a good idea of course, |
| 252 |
# but the practical effect has been confusing and many difficult bugs have been caused |
| 253 |
# by the opacity of the native libraries, and their variable behavior across platforms. |
| 254 |
# |
| 255 |
# In general, Net::LDAP instance methods which invoke server operations make a connection |
| 256 |
# to the server when the method is called. They execute the operation (typically binding first) |
| 257 |
# and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection |
| 258 |
# to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open |
| 259 |
# closes the connection on completion of the block. |
| 260 |
# |
| 261 |
|
| 262 |
class LDAP |
| 263 |
|
| 264 |
class LdapError < Exception; end |
| 265 |
|
| 266 |
VERSION = "0.0.4" |
| 267 |
|
| 268 |
|
| 269 |
SearchScope_BaseObject = 0 |
| 270 |
SearchScope_SingleLevel = 1 |
| 271 |
SearchScope_WholeSubtree = 2 |
| 272 |
SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree] |
| 273 |
|
| 274 |
AsnSyntax = {
|
| 275 |
:application => {
|
| 276 |
:constructed => {
|
| 277 |
0 => :array, # BindRequest |
| 278 |
1 => :array, # BindResponse |
| 279 |
2 => :array, # UnbindRequest |
| 280 |
3 => :array, # SearchRequest |
| 281 |
4 => :array, # SearchData |
| 282 |
5 => :array, # SearchResult |
| 283 |
6 => :array, # ModifyRequest |
| 284 |
7 => :array, # ModifyResponse |
| 285 |
8 => :array, # AddRequest |
| 286 |
9 => :array, # AddResponse |
| 287 |
10 => :array, # DelRequest |
| 288 |
11 => :array, # DelResponse |
| 289 |
12 => :array, # ModifyRdnRequest |
| 290 |
13 => :array, # ModifyRdnResponse |
| 291 |
14 => :array, # CompareRequest |
| 292 |
15 => :array, # CompareResponse |
| 293 |
16 => :array, # AbandonRequest |
| 294 |
19 => :array, # SearchResultReferral |
| 295 |
24 => :array, # Unsolicited Notification |
| 296 |
} |
| 297 |
}, |
| 298 |
:context_specific => {
|
| 299 |
:primitive => {
|
| 300 |
0 => :string, # password |
| 301 |
1 => :string, # Kerberos v4 |
| 302 |
2 => :string, # Kerberos v5 |
| 303 |
}, |
| 304 |
:constructed => {
|
| 305 |
0 => :array, # RFC-2251 Control |
| 306 |
3 => :array, # Seach referral |
| 307 |
} |
| 308 |
} |
| 309 |
} |
| 310 |
|
| 311 |
DefaultHost = "127.0.0.1" |
| 312 |
DefaultPort = 389 |
| 313 |
DefaultAuth = {:method => :anonymous}
|
| 314 |
DefaultTreebase = "dc=com" |
| 315 |
|
| 316 |
|
| 317 |
ResultStrings = {
|
| 318 |
0 => "Success", |
| 319 |
1 => "Operations Error", |
| 320 |
2 => "Protocol Error", |
| 321 |
3 => "Time Limit Exceeded", |
| 322 |
4 => "Size Limit Exceeded", |
| 323 |
12 => "Unavailable crtical extension", |
| 324 |
16 => "No Such Attribute", |
| 325 |
17 => "Undefined Attribute Type", |
| 326 |
20 => "Attribute or Value Exists", |
| 327 |
32 => "No Such Object", |
| 328 |
34 => "Invalid DN Syntax", |
| 329 |
48 => "Invalid DN Syntax", |
| 330 |
48 => "Inappropriate Authentication", |
| 331 |
49 => "Invalid Credentials", |
| 332 |
50 => "Insufficient Access Rights", |
| 333 |
51 => "Busy", |
| 334 |
52 => "Unavailable", |
| 335 |
53 => "Unwilling to perform", |
| 336 |
65 => "Object Class Violation", |
| 337 |
68 => "Entry Already Exists" |
| 338 |
} |
| 339 |
|
| 340 |
|
| 341 |
module LdapControls |
| 342 |
PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696 |
| 343 |
end |
| 344 |
|
| 345 |
|
| 346 |
# |
| 347 |
# LDAP::result2string |
| 348 |
# |
| 349 |
def LDAP::result2string code # :nodoc: |
| 350 |
ResultStrings[code] || "unknown result (#{code})"
|
| 351 |
end |
| 352 |
|
| 353 |
|
| 354 |
attr_accessor :host, :port, :base |
| 355 |
|
| 356 |
|
| 357 |
# Instantiate an object of type Net::LDAP to perform directory operations. |
| 358 |
# This constructor takes a Hash containing arguments, all of which are either optional or may be specified later with other methods as described below. The following arguments |
| 359 |
# are supported: |
| 360 |
# * :host => the LDAP server's IP-address (default 127.0.0.1) |
| 361 |
# * :port => the LDAP server's TCP port (default 389) |
| 362 |
# * :auth => a Hash containing authorization parameters. Currently supported values include: |
| 363 |
# {:method => :anonymous} and
|
| 364 |
# {:method => :simple, :username => your_user_name, :password => your_password }
|
| 365 |
# The password parameter may be a Proc that returns a String. |
| 366 |
# * :base => a default treebase parameter for searches performed against the LDAP server. If you don't give this value, then each call to #search must specify a treebase parameter. If you do give this value, then it will be used in subsequent calls to #search that do not specify a treebase. If you give a treebase value in any particular call to #search, that value will override any treebase value you give here. |
| 367 |
# * :encryption => specifies the encryption to be used in communicating with the LDAP server. The value is either a Hash containing additional parameters, or the Symbol :simple_tls, which is equivalent to specifying the Hash {:method => :simple_tls}. There is a fairly large range of potential values that may be given for this parameter. See #encryption for details.
|
| 368 |
# |
| 369 |
# Instantiating a Net::LDAP object does <i>not</i> result in network traffic to |
| 370 |
# the LDAP server. It simply stores the connection and binding parameters in the |
| 371 |
# object. |
| 372 |
# |
| 373 |
def initialize args = {}
|
| 374 |
@host = args[:host] || DefaultHost |
| 375 |
@port = args[:port] || DefaultPort |
| 376 |
@verbose = false # Make this configurable with a switch on the class. |
| 377 |
@auth = args[:auth] || DefaultAuth |
| 378 |
@base = args[:base] || DefaultTreebase |
| 379 |
encryption args[:encryption] # may be nil |
| 380 |
|
| 381 |
if pr = @auth[:password] and pr.respond_to?(:call) |
| 382 |
@auth[:password] = pr.call |
| 383 |
end |
| 384 |
|
| 385 |
# This variable is only set when we are created with LDAP::open. |
| 386 |
# All of our internal methods will connect using it, or else |
| 387 |
# they will create their own. |
| 388 |
@open_connection = nil |
| 389 |
end |
| 390 |
|
| 391 |
# Convenience method to specify authentication credentials to the LDAP |
| 392 |
# server. Currently supports simple authentication requiring |
| 393 |
# a username and password. |
| 394 |
# |
| 395 |
# Observe that on most LDAP servers, |
| 396 |
# the username is a complete DN. However, with A/D, it's often possible |
| 397 |
# to give only a user-name rather than a complete DN. In the latter |
| 398 |
# case, beware that many A/D servers are configured to permit anonymous |
| 399 |
# (uncredentialled) binding, and will silently accept your binding |
| 400 |
# as anonymous if you give an unrecognized username. This is not usually |
| 401 |
# what you want. (See #get_operation_result.) |
| 402 |
# |
| 403 |
# <b>Important:</b> The password argument may be a Proc that returns a string. |
| 404 |
# This makes it possible for you to write client programs that solicit |
| 405 |
# passwords from users or from other data sources without showing them |
| 406 |
# in your code or on command lines. |
| 407 |
# |
| 408 |
# require 'net/ldap' |
| 409 |
# |
| 410 |
# ldap = Net::LDAP.new |
| 411 |
# ldap.host = server_ip_address |
| 412 |
# ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw" |
| 413 |
# |
| 414 |
# Alternatively (with a password block): |
| 415 |
# |
| 416 |
# require 'net/ldap' |
| 417 |
# |
| 418 |
# ldap = Net::LDAP.new |
| 419 |
# ldap.host = server_ip_address |
| 420 |
# psw = proc { your_psw_function }
|
| 421 |
# ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", psw |
| 422 |
# |
| 423 |
def authenticate username, password |
| 424 |
password = password.call if password.respond_to?(:call) |
| 425 |
@auth = {:method => :simple, :username => username, :password => password}
|
| 426 |
end |
| 427 |
|
| 428 |
alias_method :auth, :authenticate |
| 429 |
|
| 430 |
# Convenience method to specify encryption characteristics for connections |
| 431 |
# to LDAP servers. Called implicitly by #new and #open, but may also be called |
| 432 |
# by user code if desired. |
| 433 |
# The single argument is generally a Hash (but see below for convenience alternatives). |
| 434 |
# This implementation is currently a stub, supporting only a few encryption |
| 435 |
# alternatives. As additional capabilities are added, more configuration values |
| 436 |
# will be added here. |
| 437 |
# |
| 438 |
# Currently, the only supported argument is {:method => :simple_tls}.
|
| 439 |
# (Equivalently, you may pass the symbol :simple_tls all by itself, without |
| 440 |
# enclosing it in a Hash.) |
| 441 |
# |
| 442 |
# The :simple_tls encryption method encrypts <i>all</i> communications with the LDAP |
| 443 |
# server. |
| 444 |
# It completely establishes SSL/TLS encryption with the LDAP server |
| 445 |
# before any LDAP-protocol data is exchanged. |
| 446 |
# There is no plaintext negotiation and no special encryption-request controls |
| 447 |
# are sent to the server. |
| 448 |
# <i>The :simple_tls option is the simplest, easiest way to encrypt communications |
| 449 |
# between Net::LDAP and LDAP servers.</i> |
| 450 |
# It's intended for cases where you have an implicit level of trust in the authenticity |
| 451 |
# of the LDAP server. No validation of the LDAP server's SSL certificate is |
| 452 |
# performed. This means that :simple_tls will not produce errors if the LDAP |
| 453 |
# server's encryption certificate is not signed by a well-known Certification |
| 454 |
# Authority. |
| 455 |
# If you get communications or protocol errors when using this option, check |
| 456 |
# with your LDAP server administrator. Pay particular attention to the TCP port |
| 457 |
# you are connecting to. It's impossible for an LDAP server to support plaintext |
| 458 |
# LDAP communications and <i>simple TLS</i> connections on the same port. |
| 459 |
# The standard TCP port for unencrypted LDAP connections is 389, but the standard |
| 460 |
# port for simple-TLS encrypted connections is 636. Be sure you are using the |
| 461 |
# correct port. |
| 462 |
# |
| 463 |
# <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP control, |
| 464 |
# which will enable encrypted communications on the same TCP port used for |
| 465 |
# unencrypted connections.]</i> |
| 466 |
# |
| 467 |
def encryption args |
| 468 |
if args == :simple_tls |
| 469 |
args = {:method => :simple_tls}
|
| 470 |
end |
| 471 |
@encryption = args |
| 472 |
end |
| 473 |
|
| 474 |
|
| 475 |
# #open takes the same parameters as #new. #open makes a network connection to the |
| 476 |
# LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block. |
| 477 |
# Within the block, you can call any of the instance methods of Net::LDAP to |
| 478 |
# perform operations against the LDAP directory. #open will perform all the |
| 479 |
# operations in the user-supplied block on the same network connection, which |
| 480 |
# will be closed automatically when the block finishes. |
| 481 |
# |
| 482 |
# # (PSEUDOCODE) |
| 483 |
# auth = {:method => :simple, :username => username, :password => password}
|
| 484 |
# Net::LDAP.open( :host => ipaddress, :port => 389, :auth => auth ) do |ldap| |
| 485 |
# ldap.search( ... ) |
| 486 |
# ldap.add( ... ) |
| 487 |
# ldap.modify( ... ) |
| 488 |
# end |
| 489 |
# |
| 490 |
def LDAP::open args |
| 491 |
ldap1 = LDAP.new args |
| 492 |
ldap1.open {|ldap| yield ldap }
|
| 493 |
end |
| 494 |
|
| 495 |
# Returns a meaningful result any time after |
| 496 |
# a protocol operation (#bind, #search, #add, #modify, #rename, #delete) |
| 497 |
# has completed. |
| 498 |
# It returns an #OpenStruct containing an LDAP result code (0 means success), |
| 499 |
# and a human-readable string. |
| 500 |
# unless ldap.bind |
| 501 |
# puts "Result: #{ldap.get_operation_result.code}"
|
| 502 |
# puts "Message: #{ldap.get_operation_result.message}"
|
| 503 |
# end |
| 504 |
# |
| 505 |
def get_operation_result |
| 506 |
os = OpenStruct.new |
| 507 |
if @result |
| 508 |
os.code = @result |
| 509 |
else |
| 510 |
os.code = 0 |
| 511 |
end |
| 512 |
os.message = LDAP.result2string( os.code ) |
| 513 |
os |
| 514 |
end |
| 515 |
|
| 516 |
|
| 517 |
# Opens a network connection to the server and then |
| 518 |
# passes <tt>self</tt> to the caller-supplied block. The connection is |
| 519 |
# closed when the block completes. Used for executing multiple |
| 520 |
# LDAP operations without requiring a separate network connection |
| 521 |
# (and authentication) for each one. |
| 522 |
# <i>Note:</i> You do not need to log-in or "bind" to the server. This will |
| 523 |
# be done for you automatically. |
| 524 |
# For an even simpler approach, see the class method Net::LDAP#open. |
| 525 |
# |
| 526 |
# # (PSEUDOCODE) |
| 527 |
# auth = {:method => :simple, :username => username, :password => password}
|
| 528 |
# ldap = Net::LDAP.new( :host => ipaddress, :port => 389, :auth => auth ) |
| 529 |
# ldap.open do |ldap| |
| 530 |
# ldap.search( ... ) |
| 531 |
# ldap.add( ... ) |
| 532 |
# ldap.modify( ... ) |
| 533 |
# end |
| 534 |
#-- |
| 535 |
# First we make a connection and then a binding, but we don't |
| 536 |
# do anything with the bind results. |
| 537 |
# We then pass self to the caller's block, where he will execute |
| 538 |
# his LDAP operations. Of course they will all generate auth failures |
| 539 |
# if the bind was unsuccessful. |
| 540 |
def open |
| 541 |
raise LdapError.new( "open already in progress" ) if @open_connection |
| 542 |
@open_connection = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) |
| 543 |
@open_connection.bind @auth |
| 544 |
yield self |
| 545 |
@open_connection.close |
| 546 |
@open_connection = nil |
| 547 |
end |
| 548 |
|
| 549 |
|
| 550 |
# Searches the LDAP directory for directory entries. |
| 551 |
# Takes a hash argument with parameters. Supported parameters include: |
| 552 |
# * :base (a string specifying the tree-base for the search); |
| 553 |
# * :filter (an object of type Net::LDAP::Filter, defaults to objectclass=*); |
| 554 |
# * :attributes (a string or array of strings specifying the LDAP attributes to return from the server); |
| 555 |
# * :return_result (a boolean specifying whether to return a result set). |
| 556 |
# * :attributes_only (a boolean flag, defaults false) |
| 557 |
# * :scope (one of: Net::LDAP::SearchScope_BaseObject, Net::LDAP::SearchScope_SingleLevel, Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.) |
| 558 |
# |
| 559 |
# #search queries the LDAP server and passes <i>each entry</i> to the |
| 560 |
# caller-supplied block, as an object of type Net::LDAP::Entry. |
| 561 |
# If the search returns 1000 entries, the block will |
| 562 |
# be called 1000 times. If the search returns no entries, the block will |
| 563 |
# not be called. |
| 564 |
# |
| 565 |
#-- |
| 566 |
# ORIGINAL TEXT, replaced 04May06. |
| 567 |
# #search returns either a result-set or a boolean, depending on the |
| 568 |
# value of the <tt>:return_result</tt> argument. The default behavior is to return |
| 569 |
# a result set, which is a hash. Each key in the hash is a string specifying |
| 570 |
# the DN of an entry. The corresponding value for each key is a Net::LDAP::Entry object. |
| 571 |
# If you request a result set and #search fails with an error, it will return nil. |
| 572 |
# Call #get_operation_result to get the error information returned by |
| 573 |
# the LDAP server. |
| 574 |
#++ |
| 575 |
# #search returns either a result-set or a boolean, depending on the |
| 576 |
# value of the <tt>:return_result</tt> argument. The default behavior is to return |
| 577 |
# a result set, which is an Array of objects of class Net::LDAP::Entry. |
| 578 |
# If you request a result set and #search fails with an error, it will return nil. |
| 579 |
# Call #get_operation_result to get the error information returned by |
| 580 |
# the LDAP server. |
| 581 |
# |
| 582 |
# When <tt>:return_result => false,</tt> #search will |
| 583 |
# return only a Boolean, to indicate whether the operation succeeded. This can improve performance |
| 584 |
# with very large result sets, because the library can discard each entry from memory after |
| 585 |
# your block processes it. |
| 586 |
# |
| 587 |
# |
| 588 |
# treebase = "dc=example,dc=com" |
| 589 |
# filter = Net::LDAP::Filter.eq( "mail", "a*.com" ) |
| 590 |
# attrs = ["mail", "cn", "sn", "objectclass"] |
| 591 |
# ldap.search( :base => treebase, :filter => filter, :attributes => attrs, :return_result => false ) do |entry| |
| 592 |
# puts "DN: #{entry.dn}"
|
| 593 |
# entry.each do |attr, values| |
| 594 |
# puts ".......#{attr}:"
|
| 595 |
# values.each do |value| |
| 596 |
# puts " #{value}"
|
| 597 |
# end |
| 598 |
# end |
| 599 |
# end |
| 600 |
# |
| 601 |
#-- |
| 602 |
# This is a re-implementation of search that replaces the |
| 603 |
# original one (now renamed searchx and possibly destined to go away). |
| 604 |
# The difference is that we return a dataset (or nil) from the |
| 605 |
# call, and pass _each entry_ as it is received from the server |
| 606 |
# to the caller-supplied block. This will probably make things |
| 607 |
# far faster as we can do useful work during the network latency |
| 608 |
# of the search. The downside is that we have no access to the |
| 609 |
# whole set while processing the blocks, so we can't do stuff |
| 610 |
# like sort the DNs until after the call completes. |
| 611 |
# It's also possible that this interacts badly with server timeouts. |
| 612 |
# We'll have to ensure that something reasonable happens if |
| 613 |
# the caller has processed half a result set when we throw a timeout |
| 614 |
# error. |
| 615 |
# Another important difference is that we return a result set from |
| 616 |
# this method rather than a T/F indication. |
| 617 |
# Since this can be very heavy-weight, we define an argument flag |
| 618 |
# that the caller can set to suppress the return of a result set, |
| 619 |
# if he's planning to process every entry as it comes from the server. |
| 620 |
# |
| 621 |
# REINTERPRETED the result set, 04May06. Originally this was a hash |
| 622 |
# of entries keyed by DNs. But let's get away from making users |
| 623 |
# handle DNs. Change it to a plain array. Eventually we may |
| 624 |
# want to return a Dataset object that delegates to an internal |
| 625 |
# array, so we can provide sort methods and what-not. |
| 626 |
# |
| 627 |
def search args = {}
|
| 628 |
args[:base] ||= @base |
| 629 |
result_set = (args and args[:return_result] == false) ? nil : [] |
| 630 |
|
| 631 |
if @open_connection |
| 632 |
@result = @open_connection.search( args ) {|entry|
|
| 633 |
result_set << entry if result_set |
| 634 |
yield( entry ) if block_given? |
| 635 |
} |
| 636 |
else |
| 637 |
@result = 0 |
| 638 |
conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) |
| 639 |
if (@result = conn.bind( args[:auth] || @auth )) == 0 |
| 640 |
@result = conn.search( args ) {|entry|
|
| 641 |
result_set << entry if result_set |
| 642 |
yield( entry ) if block_given? |
| 643 |
} |
| 644 |
end |
| 645 |
conn.close |
| 646 |
end |
| 647 |
|
| 648 |
@result == 0 and result_set |
| 649 |
end |
| 650 |
|
| 651 |
# #bind connects to an LDAP server and requests authentication |
| 652 |
# based on the <tt>:auth</tt> parameter passed to #open or #new. |
| 653 |
# It takes no parameters. |
| 654 |
# |
| 655 |
# User code does not need to call #bind directly. It will be called |
| 656 |
# implicitly by the library whenever you invoke an LDAP operation, |
| 657 |
# such as #search or #add. |
| 658 |
# |
| 659 |
# It is useful, however, to call #bind in your own code when the |
| 660 |
# only operation you intend to perform against the directory is |
| 661 |
# to validate a login credential. #bind returns true or false |
| 662 |
# to indicate whether the binding was successful. Reasons for |
| 663 |
# failure include malformed or unrecognized usernames and |
| 664 |
# incorrect passwords. Use #get_operation_result to find out |
| 665 |
# what happened in case of failure. |
| 666 |
# |
| 667 |
# Here's a typical example using #bind to authenticate a |
| 668 |
# credential which was (perhaps) solicited from the user of a |
| 669 |
# web site: |
| 670 |
# |
| 671 |
# require 'net/ldap' |
| 672 |
# ldap = Net::LDAP.new |
| 673 |
# ldap.host = your_server_ip_address |
| 674 |
# ldap.port = 389 |
| 675 |
# ldap.auth your_user_name, your_user_password |
| 676 |
# if ldap.bind |
| 677 |
# # authentication succeeded |
| 678 |
# else |
| 679 |
# # authentication failed |
| 680 |
# p ldap.get_operation_result |
| 681 |
# end |
| 682 |
# |
| 683 |
# You don't have to create a new instance of Net::LDAP every time |
| 684 |
# you perform a binding in this way. If you prefer, you can cache the Net::LDAP object |
| 685 |
# and re-use it to perform subsequent bindings, <i>provided</i> you call |
| 686 |
# #auth to specify a new credential before calling #bind. Otherwise, you'll |
| 687 |
# just re-authenticate the previous user! (You don't need to re-set |
| 688 |
# the values of #host and #port.) As noted in the documentation for #auth, |
| 689 |
# the password parameter can be a Ruby Proc instead of a String. |
| 690 |
# |
| 691 |
#-- |
| 692 |
# If there is an @open_connection, then perform the bind |
| 693 |
# on it. Otherwise, connect, bind, and disconnect. |
| 694 |
# The latter operation is obviously useful only as an auth check. |
| 695 |
# |
| 696 |
def bind auth=@auth |
| 697 |
if @open_connection |
| 698 |
@result = @open_connection.bind auth |
| 699 |
else |
| 700 |
conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption) |
| 701 |
@result = conn.bind @auth |
| 702 |
conn.close |
| 703 |
end |
| 704 |
|
| 705 |
@result == 0 |
| 706 |
end |
| 707 |
|
| 708 |
# |
| 709 |
# #bind_as is for testing authentication credentials. |
| 710 |
# |
| 711 |
# As described under #bind, most LDAP servers require that you supply a complete DN |
| 712 |
# as a binding-credential, along with an authenticator such as a password. |
| 713 |
# But for many applications (such as authenticating users to a Rails application), |
| 714 |
# you often don't have a full DN to identify the user. You usually get a simple |
| 715 |
# identifier like a username or an email address, along with a password. |
| 716 |
# #bind_as allows you to authenticate these user-identifiers. |
| 717 |
# |
| 718 |
# #bind_as is a combination of a search and an LDAP binding. First, it connects and |
| 719 |
# binds to the directory as normal. Then it searches the directory for an entry |
| 720 |
# corresponding to the email address, username, or other string that you supply. |
| 721 |
# If the entry exists, then #bind_as will <b>re-bind</b> as that user with the |
| 722 |
# password (or other authenticator) that you supply. |
| 723 |
# |
| 724 |
# #bind_as takes the same parameters as #search, <i>with the addition of an |
| 725 |
# authenticator.</i> Currently, this authenticator must be <tt>:password</tt>. |
| 726 |
# Its value may be either a String, or a +proc+ that returns a String. |
| 727 |
# #bind_as returns +false+ on failure. On success, it returns a result set, |
| 728 |
# just as #search does. This result set is an Array of objects of |
| 729 |
# type Net::LDAP::Entry. It contains the directory attributes corresponding to |
| 730 |
# the user. (Just test whether the return value is logically true, if you don't |
| 731 |
# need this additional information.) |
| 732 |
# |
| 733 |
# Here's how you would use #bind_as to authenticate an email address and password: |
| 734 |
# |
| 735 |
# require 'net/ldap' |
| 736 |
# |
| 737 |
# user,psw = "joe_user@yourcompany.com", "joes_psw" |
| 738 |
# |
| 739 |
# ldap = Net::LDAP.new |
| 740 |
# ldap.host = "192.168.0.100" |
| 741 |
# ldap.port = 389 |
| 742 |
# ldap.auth "cn=manager,dc=yourcompany,dc=com", "topsecret" |
| 743 |
# |
| 744 |
# result = ldap.bind_as( |
| 745 |
# :base => "dc=yourcompany,dc=com", |
| 746 |
# :filter => "(mail=#{user})",
|
| 747 |
# :password => psw |
| 748 |
# ) |
| 749 |
# if result |
| 750 |
# puts "Authenticated #{result.first.dn}"
|
| 751 |
# else |
| 752 |
# puts "Authentication FAILED." |
| 753 |
# end |
| 754 |
def bind_as args={}
|
| 755 |
result = false |
| 756 |
open {|me|
|
| 757 |
rs = search args |
| 758 |
if rs and rs.first and dn = rs.first.dn |
| 759 |
password = args[:password] |
| 760 |
password = password.call if password.respond_to?(:call) |
| 761 |
result = rs if bind :method => :simple, :username => dn, :password => password |
| 762 |
end |
| 763 |
} |
| 764 |
result |
| 765 |
end |
| 766 |
|
| 767 |
|
| 768 |
# Adds a new entry to the remote LDAP server. |
| 769 |
# Supported arguments: |
| 770 |
# :dn :: Full DN of the new entry |
| 771 |
# :attributes :: Attributes of the new entry. |
| 772 |
# |
| 773 |
# The attributes argument is supplied as a Hash keyed by Strings or Symbols |
| 774 |
# giving the attribute name, and mapping to Strings or Arrays of Strings |
| 775 |
# giving the actual attribute values. Observe that most LDAP directories |
| 776 |
# enforce schema constraints on the attributes contained in entries. |
| 777 |
# #add will fail with a server-generated error if your attributes violate |
| 778 |
# the server-specific constraints. |
| 779 |
# Here's an example: |
| 780 |
# |
| 781 |
# dn = "cn=George Smith,ou=people,dc=example,dc=com" |
| 782 |
# attr = {
|
| 783 |
# :cn => "George Smith", |
| 784 |
# :objectclass => ["top", "inetorgperson"], |
| 785 |
# :sn => "Smith", |
| 786 |
# :mail => "gsmith@example.com" |
| 787 |
# } |
| 788 |
# Net::LDAP.open (:host => host) do |ldap| |
| 789 |
# ldap.add( :dn => dn, :attributes => attr ) |
| 790 |
# end |
| 791 |
# |
| 792 |
def add args |
| 793 |
if @open_connection |
| 794 |
@result = @open_connection.add( args ) |
| 795 |
else |
| 796 |
@result = 0 |
| 797 |
conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption) |
| 798 |
if (@result = conn.bind( args[:auth] || @auth )) == 0 |
| 799 |
@result = conn.add( args ) |
| 800 |
end |
| 801 |
conn.close |
| 802 |
end |
| 803 |
@result == 0 |
| 804 |
end |
| 805 |
|
| 806 |
|
| 807 |
# Modifies the attribute values of a particular entry on the LDAP directory. |
| 808 |
# Takes a hash with arguments. Supported arguments are: |
| 809 |
# :dn :: (the full DN of the entry whose attributes are to be modified) |
| 810 |
# :operations :: (the modifications to be performed, detailed next) |
| 811 |
# |
| 812 |
# This method returns True or False to indicate whether the operation |
| 813 |
# succeeded or failed, with extended information available by calling |
| 814 |
# #get_operation_result. |
| 815 |
# |
| 816 |
# Also see #add_attribute, #replace_attribute, or #delete_attribute, which |
| 817 |
# provide simpler interfaces to this functionality. |
| 818 |
# |
| 819 |
# The LDAP protocol provides a full and well thought-out set of operations |
| 820 |
# for changing the values of attributes, but they are necessarily somewhat complex |
| 821 |
# and not always intuitive. If these instructions are confusing or incomplete, |
| 822 |
# please send us email or create a bug report on rubyforge. |
| 823 |
# |
| 824 |
# The :operations parameter to #modify takes an array of operation-descriptors. |
| 825 |
# Each individual operation is specified in one element of the array, and |
| 826 |
# most LDAP servers will attempt to perform the operations in order. |
| 827 |
# |
| 828 |
# Each of the operations appearing in the Array must itself be an Array |
| 829 |
# with exactly three elements: |
| 830 |
# an operator:: must be :add, :replace, or :delete |
| 831 |
# an attribute name:: the attribute name (string or symbol) to modify |
| 832 |
# a value:: either a string or an array of strings. |
| 833 |
# |
| 834 |
# The :add operator will, unsurprisingly, add the specified values to |
| 835 |
# the specified attribute. If the attribute does not already exist, |
| 836 |
# :add will create it. Most LDAP servers will generate an error if you |
| 837 |
# try to add a value that already exists. |
| 838 |
# |
| 839 |
# :replace will erase the current value(s) for the specified attribute, |
| 840 |
# if there are any, and replace them with the specified value(s). |
| 841 |
# |
| 842 |
# :delete will remove the specified value(s) from the specified attribute. |
| 843 |
# If you pass nil, an empty string, or an empty array as the value parameter |
| 844 |
# to a :delete operation, the _entire_ _attribute_ will be deleted, along |
| 845 |
# with all of its values. |
| 846 |
# |
| 847 |
# For example: |
| 848 |
# |
| 849 |
# dn = "mail=modifyme@example.com,ou=people,dc=example,dc=com" |
| 850 |
# ops = [ |
| 851 |
# [:add, :mail, "aliasaddress@example.com"], |
| 852 |
# [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]], |
| 853 |
# [:delete, :sn, nil] |
| 854 |
# ] |
| 855 |
# ldap.modify :dn => dn, :operations => ops |
| 856 |
# |
| 857 |
# <i>(This example is contrived since you probably wouldn't add a mail |
| 858 |
# value right before replacing the whole attribute, but it shows that order |
| 859 |
# of execution matters. Also, many LDAP servers won't let you delete SN |
| 860 |
# because that would be a schema violation.)</i> |
| 861 |
# |
| 862 |
# It's essential to keep in mind that if you specify more than one operation in |
| 863 |
# a call to #modify, most LDAP servers will attempt to perform all of the operations |
| 864 |
# in the order you gave them. |
| 865 |
# This matters because you may specify operations on the |
| 866 |
# same attribute which must be performed in a certain order. |
| 867 |
# |
| 868 |
# Most LDAP servers will _stop_ processing your modifications if one of them |
| 869 |
# causes an error on the server (such as a schema-constraint violation). |
| 870 |
# If this happens, you will probably get a result code from the server that |
| 871 |
# reflects only the operation that failed, and you may or may not get extended |
| 872 |
# information that will tell you which one failed. #modify has no notion |
| 873 |
# of an atomic transaction. If you specify a chain of modifications in one |
| 874 |
# call to #modify, and one of them fails, the preceding ones will usually |
| 875 |
# not be "rolled back," resulting in a partial update. This is a limitation |
| 876 |
# of the LDAP protocol, not of Net::LDAP. |
| 877 |
# |
| 878 |
# The lack of transactional atomicity in LDAP means that you're usually |
| 879 |
# better off using the convenience methods #add_attribute, #replace_attribute, |
| 880 |
# and #delete_attribute, which are are wrappers over #modify. However, certain |
| 881 |
# LDAP servers may provide concurrency semantics, in which the several operations |
| 882 |
# contained in a single #modify call are not interleaved with other |
| 883 |
# modification-requests received simultaneously by the server. |
| 884 |
# It bears repeating that this concurrency does _not_ imply transactional |
| 885 |
# atomicity, which LDAP does not provide. |
| 886 |
# |
| 887 |
def modify args |
| 888 |
if @open_connection |
| 889 |
@result = @open_connection.modify( args ) |
| 890 |
else |
| 891 |
@result = 0 |
| 892 |
conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) |
| 893 |
if (@result = conn.bind( args[:auth] || @auth )) == 0 |
| 894 |
@result = conn.modify( args ) |
| 895 |
end |
| 896 |
conn.close |
| 897 |
end |
| 898 |
@result == 0 |
| 899 |
end |
| 900 |
|
| 901 |
|
| 902 |
# Add a value to an attribute. |
| 903 |
# Takes the full DN of the entry to modify, |
| 904 |
# the name (Symbol or String) of the attribute, and the value (String or |
| 905 |
# Array). If the attribute does not exist (and there are no schema violations), |
| 906 |
# #add_attribute will create it with the caller-specified values. |
| 907 |
# If the attribute already exists (and there are no schema violations), the |
| 908 |
# caller-specified values will be _added_ to the values already present. |
| 909 |
# |
| 910 |
# Returns True or False to indicate whether the operation |
| 911 |
# succeeded or failed, with extended information available by calling |
| 912 |
# #get_operation_result. See also #replace_attribute and #delete_attribute. |
| 913 |
# |
| 914 |
# dn = "cn=modifyme,dc=example,dc=com" |
| 915 |
# ldap.add_attribute dn, :mail, "newmailaddress@example.com" |
| 916 |
# |
| 917 |
def add_attribute dn, attribute, value |
| 918 |
modify :dn => dn, :operations => [[:add, attribute, value]] |
| 919 |
end |
| 920 |
|
| 921 |
# Replace the value of an attribute. |
| 922 |
# #replace_attribute can be thought of as equivalent to calling #delete_attribute |
| 923 |
# followed by #add_attribute. It takes the full DN of the entry to modify, |
| 924 |
# the name (Symbol or String) of the attribute, and the value (String or |
| 925 |
# Array). If the attribute does not exist, it will be created with the |
| 926 |
# caller-specified value(s). If the attribute does exist, its values will be |
| 927 |
# _discarded_ and replaced with the caller-specified values. |
| 928 |
# |
| 929 |
# Returns True or False to indicate whether the operation |
| 930 |
# succeeded or failed, with extended information available by calling |
| 931 |
# #get_operation_result. See also #add_attribute and #delete_attribute. |
| 932 |
# |
| 933 |
# dn = "cn=modifyme,dc=example,dc=com" |
| 934 |
# ldap.replace_attribute dn, :mail, "newmailaddress@example.com" |
| 935 |
# |
| 936 |
def replace_attribute dn, attribute, value |
| 937 |
modify :dn => dn, :operations => [[:replace, attribute, value]] |
| 938 |
end |
| 939 |
|
| 940 |
# Delete an attribute and all its values. |
| 941 |
# Takes the full DN of the entry to modify, and the |
| 942 |
# name (Symbol or String) of the attribute to delete. |
| 943 |
# |
| 944 |
# Returns True or False to indicate whether the operation |
| 945 |
# succeeded or failed, with extended information available by calling |
| 946 |
# #get_operation_result. See also #add_attribute and #replace_attribute. |
| 947 |
# |
| 948 |
# dn = "cn=modifyme,dc=example,dc=com" |
| 949 |
# ldap.delete_attribute dn, :mail |
| 950 |
# |
| 951 |
def delete_attribute dn, attribute |
| 952 |
modify :dn => dn, :operations => [[:delete, attribute, nil]] |
| 953 |
end |
| 954 |
|
| 955 |
|
| 956 |
# Rename an entry on the remote DIS by changing the last RDN of its DN. |
| 957 |
# _Documentation_ _stub_ |
| 958 |
# |
| 959 |
def rename args |
| 960 |
if @open_connection |
| 961 |
@result = @open_connection.rename( args ) |
| 962 |
else |
| 963 |
@result = 0 |
| 964 |
conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) |
| 965 |
if (@result = conn.bind( args[:auth] || @auth )) == 0 |
| 966 |
@result = conn.rename( args ) |
| 967 |
end |
| 968 |
conn.close |
| 969 |
end |
| 970 |
@result == 0 |
| 971 |
end |
| 972 |
|
| 973 |
# modify_rdn is an alias for #rename. |
| 974 |
def modify_rdn args |
| 975 |
rename args |
| 976 |
end |
| 977 |
|
| 978 |
# Delete an entry from the LDAP directory. |
| 979 |
# Takes a hash of arguments. |
| 980 |
# The only supported argument is :dn, which must |
| 981 |
# give the complete DN of the entry to be deleted. |
| 982 |
# Returns True or False to indicate whether the delete |
| 983 |
# succeeded. Extended status information is available by |
| 984 |
# calling #get_operation_result. |
| 985 |
# |
| 986 |
# dn = "mail=deleteme@example.com,ou=people,dc=example,dc=com" |
| 987 |
# ldap.delete :dn => dn |
| 988 |
# |
| 989 |
def delete args |
| 990 |
if @open_connection |
| 991 |
@result = @open_connection.delete( args ) |
| 992 |
else |
| 993 |
@result = 0 |
| 994 |
conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) |
| 995 |
if (@result = conn.bind( args[:auth] || @auth )) == 0 |
| 996 |
@result = conn.delete( args ) |
| 997 |
end |
| 998 |
conn.close |
| 999 |
end |
| 1000 |
@result == 0 |
| 1001 |
end |
| 1002 |
|
| 1003 |
end # class LDAP |
| 1004 |
|
| 1005 |
|
| 1006 |
|
| 1007 |
class LDAP |
| 1008 |
# This is a private class used internally by the library. It should not be called by user code. |
| 1009 |
class Connection # :nodoc: |
| 1010 |
|
| 1011 |
LdapVersion = 3 |
| 1012 |
|
| 1013 |
|
| 1014 |
#-- |
| 1015 |
# initialize |
| 1016 |
# |
| 1017 |
def initialize server |
| 1018 |
begin |
| 1019 |
@conn = TCPsocket.new( server[:host], server[:port] ) |
| 1020 |
rescue |
| 1021 |
raise LdapError.new( "no connection to server" ) |
| 1022 |
end |
| 1023 |
|
| 1024 |
if server[:encryption] |
| 1025 |
setup_encryption server[:encryption] |
| 1026 |
end |
| 1027 |
|
| 1028 |
yield self if block_given? |
| 1029 |
end |
| 1030 |
|
| 1031 |
|
| 1032 |
#-- |
| 1033 |
# Helper method called only from new, and only after we have a successfully-opened |
| 1034 |
# @conn instance variable, which is a TCP connection. |
| 1035 |
# Depending on the received arguments, we establish SSL, potentially replacing |
| 1036 |
# the value of @conn accordingly. |
| 1037 |
# Don't generate any errors here if no encryption is requested. |
| 1038 |
# DO raise LdapError objects if encryption is requested and we have trouble setting |
| 1039 |
# it up. That includes if OpenSSL is not set up on the machine. (Question: |
| 1040 |
# how does the Ruby OpenSSL wrapper react in that case?) |
| 1041 |
# DO NOT filter exceptions raised by the OpenSSL library. Let them pass back |
| 1042 |
# to the user. That should make it easier for us to debug the problem reports. |
| 1043 |
# Presumably (hopefully?) that will also produce recognizable errors if someone |
| 1044 |
# tries to use this on a machine without OpenSSL. |
| 1045 |
# |
| 1046 |
# The simple_tls method is intended as the simplest, stupidest, easiest solution |
| 1047 |
# for people who want nothing more than encrypted comms with the LDAP server. |
| 1048 |
# It doesn't do any server-cert validation and requires nothing in the way |
| 1049 |
# of key files and root-cert files, etc etc. |
| 1050 |
# OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected |
| 1051 |
# TCPsocket object. |
| 1052 |
# |
| 1053 |
def setup_encryption args |
| 1054 |
case args[:method] |
| 1055 |
when :simple_tls |
| 1056 |
raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
|
| 1057 |
ctx = OpenSSL::SSL::SSLContext.new |
| 1058 |
@conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx) |
| 1059 |
@conn.connect |
| 1060 |
@conn.sync_close = true |
| 1061 |
# additional branches requiring server validation and peer certs, etc. go here. |
| 1062 |
else |
| 1063 |
raise LdapError.new( "unsupported encryption method #{args[:method]}" )
|
| 1064 |
end |
| 1065 |
end |
| 1066 |
|
| 1067 |
#-- |
| 1068 |
# close |
| 1069 |
# This is provided as a convenience method to make |
| 1070 |
# sure a connection object gets closed without waiting |
| 1071 |
# for a GC to happen. Clients shouldn't have to call it, |
| 1072 |
# but perhaps it will come in handy someday. |
| 1073 |
def close |
| 1074 |
@conn.close |
| 1075 |
@conn = nil |
| 1076 |
end |
| 1077 |
|
| 1078 |
#-- |
| 1079 |
# next_msgid |
| 1080 |
# |
| 1081 |
def next_msgid |
| 1082 |
@msgid ||= 0 |
| 1083 |
@msgid += 1 |
| 1084 |
end |
| 1085 |
|
| 1086 |
|
| 1087 |
#-- |
| 1088 |
# bind |
| 1089 |
# |
| 1090 |
def bind auth |
| 1091 |
user,psw = case auth[:method] |
| 1092 |
when :anonymous |
| 1093 |
["",""] |
| 1094 |
when :simple |
| 1095 |
[auth[:username] || auth[:dn], auth[:password]] |
| 1096 |
end |
| 1097 |
raise LdapError.new( "invalid binding information" ) unless (user && psw) |
| 1098 |
|
| 1099 |
msgid = next_msgid.to_ber |
| 1100 |
request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0) |
| 1101 |
request_pkt = [msgid, request].to_ber_sequence |
| 1102 |
@conn.write request_pkt |
| 1103 |
|
| 1104 |
(be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) |
| 1105 |
pdu.result_code |
| 1106 |
end |
| 1107 |
|
| 1108 |
#-- |
| 1109 |
# search |
| 1110 |
# Alternate implementation, this yields each search entry to the caller |
| 1111 |
# as it are received. |
| 1112 |
# TODO, certain search parameters are hardcoded. |
| 1113 |
# TODO, if we mis-parse the server results or the results are wrong, we can block |
| 1114 |
# forever. That's because we keep reading results until we get a type-5 packet, |
| 1115 |
# which might never come. We need to support the time-limit in the protocol. |
| 1116 |
#-- |
| 1117 |
# WARNING: this code substantially recapitulates the searchx method. |
| 1118 |
# |
| 1119 |
# 02May06: Well, I added support for RFC-2696-style paged searches. |
| 1120 |
# This is used on all queries because the extension is marked non-critical. |
| 1121 |
# As far as I know, only A/D uses this, but it's required for A/D. Otherwise |
| 1122 |
# you won't get more than 1000 results back from a query. |
| 1123 |
# This implementation is kindof clunky and should probably be refactored. |
| 1124 |
# Also, is it my imagination, or are A/Ds the slowest directory servers ever??? |
| 1125 |
# |
| 1126 |
def search args = {}
|
| 1127 |
search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) |
| 1128 |
search_filter = Filter.construct(search_filter) if search_filter.is_a?(String) |
| 1129 |
search_base = (args && args[:base]) || "dc=example,dc=com" |
| 1130 |
search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
|
| 1131 |
return_referrals = args && args[:return_referrals] == true |
| 1132 |
|
| 1133 |
attributes_only = (args and args[:attributes_only] == true) |
| 1134 |
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree |
| 1135 |
raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope) |
| 1136 |
|
| 1137 |
# An interesting value for the size limit would be close to A/D's built-in |
| 1138 |
# page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes |
| 1139 |
# on anything bigger than 126. You get a silent error that is easily visible |
| 1140 |
# by running slapd in debug mode. Go figure. |
| 1141 |
rfc2696_cookie = [126, ""] |
| 1142 |
result_code = 0 |
| 1143 |
|
| 1144 |
loop {
|
| 1145 |
# should collect this into a private helper to clarify the structure |
| 1146 |
|
| 1147 |
request = [ |
| 1148 |
search_base.to_ber, |
| 1149 |
scope.to_ber_enumerated, |
| 1150 |
0.to_ber_enumerated, |
| 1151 |
0.to_ber, |
| 1152 |
0.to_ber, |
| 1153 |
attributes_only.to_ber, |
| 1154 |
search_filter.to_ber, |
| 1155 |
search_attributes.to_ber_sequence |
| 1156 |
].to_ber_appsequence(3) |
| 1157 |
|
| 1158 |
controls = [ |
| 1159 |
[ |
| 1160 |
LdapControls::PagedResults.to_ber, |
| 1161 |
false.to_ber, # criticality MUST be false to interoperate with normal LDAPs. |
| 1162 |
rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber
|
| 1163 |
].to_ber_sequence |
| 1164 |
].to_ber_contextspecific(0) |
| 1165 |
|
| 1166 |
pkt = [next_msgid.to_ber, request, controls].to_ber_sequence |
| 1167 |
@conn.write pkt |
| 1168 |
|
| 1169 |
result_code = 0 |
| 1170 |
controls = [] |
| 1171 |
|
| 1172 |
while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) |
| 1173 |
case pdu.app_tag |
| 1174 |
when 4 # search-data |
| 1175 |
yield( pdu.search_entry ) if block_given? |
| 1176 |
when 19 # search-referral |
| 1177 |
if return_referrals |
| 1178 |
if block_given? |
| 1179 |
se = Net::LDAP::Entry.new |
| 1180 |
se[:search_referrals] = (pdu.search_referrals || []) |
| 1181 |
yield se |
| 1182 |
end |
| 1183 |
end |
| 1184 |
#p pdu.referrals |
| 1185 |
when 5 # search-result |
| 1186 |
result_code = pdu.result_code |
| 1187 |
controls = pdu.result_controls |
| 1188 |
break |
| 1189 |
else |
| 1190 |
raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
|
| 1191 |
end |
| 1192 |
end |
| 1193 |
|
| 1194 |
# When we get here, we have seen a type-5 response. |
| 1195 |
# If there is no error AND there is an RFC-2696 cookie, |
| 1196 |
# then query again for the next page of results. |
| 1197 |
# If not, we're done. |
| 1198 |
# Don't screw this up or we'll break every search we do. |
| 1199 |
more_pages = false |
| 1200 |
if result_code == 0 and controls |
| 1201 |
controls.each do |c| |
| 1202 |
if c.oid == LdapControls::PagedResults |
| 1203 |
more_pages = false # just in case some bogus server sends us >1 of these. |
| 1204 |
if c.value and c.value.length > 0 |
| 1205 |
cookie = c.value.read_ber[1] |
| 1206 |
if cookie and cookie.length > 0 |
| 1207 |
rfc2696_cookie[1] = cookie |
| 1208 |
more_pages = true |
| 1209 |
end |
| 1210 |
end |
| 1211 |
end |
| 1212 |
end |
| 1213 |
end |
| 1214 |
|
| 1215 |
break unless more_pages |
| 1216 |
} # loop |
| 1217 |
|
| 1218 |
result_code |
| 1219 |
end |
| 1220 |
|
| 1221 |
|
| 1222 |
|
| 1223 |
|
| 1224 |
#-- |
| 1225 |
# modify |
| 1226 |
# TODO, need to support a time limit, in case the server fails to respond. |
| 1227 |
# TODO!!! We're throwing an exception here on empty DN. |
| 1228 |
# Should return a proper error instead, probaby from farther up the chain. |
| 1229 |
# TODO!!! If the user specifies a bogus opcode, we'll throw a |
| 1230 |
# confusing error here ("to_ber_enumerated is not defined on nil").
|
| 1231 |
# |
| 1232 |
def modify args |
| 1233 |
modify_dn = args[:dn] or raise "Unable to modify empty DN" |
| 1234 |
modify_ops = [] |
| 1235 |
a = args[:operations] and a.each {|op, attr, values|
|
| 1236 |
# TODO, fix the following line, which gives a bogus error |
| 1237 |
# if the opcode is invalid. |
| 1238 |
op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
|
| 1239 |
modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence
|
| 1240 |
} |
| 1241 |
|
| 1242 |
request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6) |
| 1243 |
pkt = [next_msgid.to_ber, request].to_ber_sequence |
| 1244 |
@conn.write pkt |
| 1245 |
|
| 1246 |
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" ) |
| 1247 |
pdu.result_code |
| 1248 |
end |
| 1249 |
|
| 1250 |
|
| 1251 |
#-- |
| 1252 |
# add |
| 1253 |
# TODO, need to support a time limit, in case the server fails to respond. |
| 1254 |
# |
| 1255 |
def add args |
| 1256 |
add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
|
| 1257 |
add_attrs = [] |
| 1258 |
a = args[:attributes] and a.each {|k,v|
|
| 1259 |
add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
|
| 1260 |
} |
| 1261 |
|
| 1262 |
request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) |
| 1263 |
pkt = [next_msgid.to_ber, request].to_ber_sequence |
| 1264 |
@conn.write pkt |
| 1265 |
|
| 1266 |
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" ) |
| 1267 |
pdu.result_code |
| 1268 |
end |
| 1269 |
|
| 1270 |
|
| 1271 |
#-- |
| 1272 |
# rename |
| 1273 |
# TODO, need to support a time limit, in case the server fails to respond. |
| 1274 |
# |
| 1275 |
def rename args |
| 1276 |
old_dn = args[:olddn] or raise "Unable to rename empty DN" |
| 1277 |
new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" |
| 1278 |
delete_attrs = args[:delete_attributes] ? true : false |
| 1279 |
|
| 1280 |
request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12) |
| 1281 |
pkt = [next_msgid.to_ber, request].to_ber_sequence |
| 1282 |
@conn.write pkt |
| 1283 |
|
| 1284 |
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" ) |
| 1285 |
pdu.result_code |
| 1286 |
end |
| 1287 |
|
| 1288 |
|
| 1289 |
#-- |
| 1290 |
# delete |
| 1291 |
# TODO, need to support a time limit, in case the server fails to respond. |
| 1292 |
# |
| 1293 |
def delete args |
| 1294 |
dn = args[:dn] or raise "Unable to delete empty DN" |
| 1295 |
|
| 1296 |
request = dn.to_s.to_ber_application_string(10) |
| 1297 |
pkt = [next_msgid.to_ber, request].to_ber_sequence |
| 1298 |
@conn.write pkt |
| 1299 |
|
| 1300 |
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" ) |
| 1301 |
pdu.result_code |
| 1302 |
end |
| 1303 |
|
| 1304 |
|
| 1305 |
end # class Connection |
| 1306 |
end # class LDAP |
| 1307 |
|
| 1308 |
|
| 1309 |
end # module Net |
| 1310 |
|
| 1311 |
|