You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ldap.rb 53KB

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