Permalink
Browse files

Migrate the experimental_netldap branch to trunk.

  • Loading branch information...
2 parents 57a4aad + 6b5548f commit b1fe518161a9849f774cf956cc991722057232e0 emiel committed Nov 18, 2008
View
272 COPYING
@@ -0,0 +1,272 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street,
+Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and
+distribute verbatim copies of this license document, but changing it is not
+allowed.
+
+ Preamble
+
+The licenses for most software are designed to take away your freedom to
+share and change it. By contrast, the GNU General Public License is
+intended to guarantee your freedom to share and change free software--to
+make sure the software is free for all its users. This General Public
+License applies to most of the Free Software Foundation's software and to
+any other program whose authors commit to using it. (Some other Free
+Software Foundation software is covered by the GNU Lesser General Public
+License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for this service if you
+wish), that you receive source code or can get it if you want it, that you
+can change the software or use pieces of it in new free programs; and that
+you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to
+deny you these rights or to ask you to surrender the rights. These
+restrictions translate to certain responsibilities for you if you distribute
+copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or
+for a fee, you must give the recipients all the rights that you have. You
+must make sure that they, too, receive or can get the source code. And you
+must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2)
+offer you this license which gives you legal permission to copy, distribute
+and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If
+the software is modified by someone else and passed on, we want its
+recipients to know that what they have is not the original, so that any
+problems introduced by others will not reflect on the original authors'
+reputations.
+
+Finally, any free program is threatened constantly by software patents. We
+wish to avoid the danger that redistributors of a free program will
+individually obtain patent licenses, in effect making the program
+proprietary. To prevent this, we have made it clear that any patent must be
+licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+ placed by the copyright holder saying it may be distributed under the
+ terms of this General Public License. The "Program", below, refers to
+ any such program or work, and a "work based on the Program" means either
+ the Program or any derivative work under copyright law: that is to say, a
+ work containing the Program or a portion of it, either verbatim or with
+ modifications and/or translated into another language. (Hereinafter,
+ translation is included without limitation in the term "modification".)
+ Each licensee is addressed as "you".
+
+ Activities other than copying, distribution and modification are not
+ covered by this License; they are outside its scope. The act of running
+ the Program is not restricted, and the output from the Program is covered
+ only if its contents constitute a work based on the Program (independent
+ of having been made by running the Program). Whether that is true depends
+ on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code
+ as you receive it, in any medium, provided that you conspicuously and
+ appropriately publish on each copy an appropriate copyright notice and
+ disclaimer of warranty; keep intact all the notices that refer to this
+ License and to the absence of any warranty; and give any other recipients
+ of the Program a copy of this License along with the Program.
+
+ You may charge a fee for the physical act of transferring a copy, and you
+ may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it,
+ thus forming a work based on the Program, and copy and distribute such
+ modifications or work under the terms of Section 1 above, provided that
+ you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices stating
+ that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in whole
+ or in part contains or is derived from the Program or any part
+ thereof, to be licensed as a whole at no charge to all third parties
+ under the terms of this License.
+
+ c) If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use
+ in the most ordinary way, to print or display an announcement
+ including an appropriate copyright notice and a notice that there is
+ no warranty (or else, saying that you provide a warranty) and that
+ users may redistribute the program under these conditions, and telling
+ the user how to view a copy of this License. (Exception: if the
+ Program itself is interactive but does not normally print such an
+ announcement, your work based on the Program is not required to print
+ an announcement.)
+
+ These requirements apply to the modified work as a whole. If
+ identifiable sections of that work are not derived from the Program, and
+ can be reasonably considered independent and separate works in
+ themselves, then this License, and its terms, do not apply to those
+ sections when you distribute them as separate works. But when you
+ distribute the same sections as part of a whole which is a work based on
+ the Program, the distribution of the whole must be on the terms of this
+ License, whose permissions for other licensees extend to the entire
+ whole, and thus to each and every part regardless of who wrote it.
+
+ Thus, it is not the intent of this section to claim rights or contest
+ your rights to work written entirely by you; rather, the intent is to
+ exercise the right to control the distribution of derivative or
+ collective works based on the Program.
+
+ In addition, mere aggregation of another work not based on the Program
+ with the Program (or with a work based on the Program) on a volume of a
+ storage or distribution medium does not bring the other work under the
+ scope of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+ Section 2) in object code or executable form under the terms of Sections
+ 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable source
+ code, which must be distributed under the terms of Sections 1 and 2
+ above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three years, to
+ give any third party, for a charge no more than your cost of
+ physically performing source distribution, a complete machine-readable
+ copy of the corresponding source code, to be distributed under the
+ terms of Sections 1 and 2 above on a medium customarily used for
+ software interchange; or,
+
+ c) Accompany it with the information you received as to the offer to
+ distribute corresponding source code. (This alternative is allowed
+ only for noncommercial distribution and only if you received the
+ program in object code or executable form with such an offer, in
+ accord with Subsection b above.)
+
+ The source code for a work means the preferred form of the work for
+ making modifications to it. For an executable work, complete source code
+ means all the source code for all modules it contains, plus any
+ associated interface definition files, plus the scripts used to control
+ compilation and installation of the executable. However, as a special
+ exception, the source code distributed need not include anything that is
+ normally distributed (in either source or binary form) with the major
+ components (compiler, kernel, and so on) of the operating system on which
+ the executable runs, unless that component itself accompanies the
+ executable.
+
+ If distribution of executable or object code is made by offering access
+ to copy from a designated place, then offering equivalent access to copy
+ the source code from the same place counts as distribution of the source
+ code, even though third parties are not compelled to copy the source
+ along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+ expressly provided under this License. Any attempt otherwise to copy,
+ modify, sublicense or distribute the Program is void, and will
+ automatically terminate your rights under this License. However, parties
+ who have received copies, or rights, from you under this License will not
+ have their licenses terminated so long as such parties remain in full
+ compliance.
+
+5. You are not required to accept this License, since you have not signed
+ it. However, nothing else grants you permission to modify or distribute
+ the Program or its derivative works. These actions are prohibited by law
+ if you do not accept this License. Therefore, by modifying or
+ distributing the Program (or any work based on the Program), you indicate
+ your acceptance of this License to do so, and all its terms and
+ conditions for copying, distributing or modifying the Program or works
+ based on it.
+
+6. Each time you redistribute the Program (or any work based on the
+ Program), the recipient automatically receives a license from the
+ original licensor to copy, distribute or modify the Program subject to
+ these terms and conditions. You may not impose any further restrictions
+ on the recipients' exercise of the rights granted herein. You are not
+ responsible for enforcing compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent
+ infringement or for any other reason (not limited to patent issues),
+ conditions are imposed on you (whether by court order, agreement or
+ otherwise) that contradict the conditions of this License, they do not
+ excuse you from the conditions of this License. If you cannot distribute
+ so as to satisfy simultaneously your obligations under this License and
+ any other pertinent obligations, then as a consequence you may not
+ distribute the Program at all. For example, if a patent license would
+ not permit royalty-free redistribution of the Program by all those who
+ receive copies directly or indirectly through you, then the only way you
+ could satisfy both it and this License would be to refrain entirely from
+ distribution of the Program.
+
+ If any portion of this section is held invalid or unenforceable under any
+ particular circumstance, the balance of the section is intended to apply
+ and the section as a whole is intended to apply in other circumstances.
+
+ It is not the purpose of this section to induce you to infringe any
+ patents or other property right claims or to contest validity of any such
+ claims; this section has the sole purpose of protecting the integrity of
+ the free software distribution system, which is implemented by public
+ license practices. Many people have made generous contributions to the
+ wide range of software distributed through that system in reliance on
+ consistent application of that system; it is up to the author/donor to
+ decide if he or she is willing to distribute software through any other
+ system and a licensee cannot impose that choice.
+
+ This section is intended to make thoroughly clear what is believed to be
+ a consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain
+ countries either by patents or by copyrighted interfaces, the original
+ copyright holder who places the Program under this License may add an
+ explicit geographical distribution limitation excluding those countries,
+ so that distribution is permitted only in or among countries not thus
+ excluded. In such case, this License incorporates the limitation as if
+ written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of
+ the General Public License from time to time. Such new versions will be
+ similar in spirit to the present version, but may differ in detail to
+ address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the Program
+ specifies a version number of this License which applies to it and "any
+ later version", you have the option of following the terms and conditions
+ either of that version or of any later version published by the Free
+ Software Foundation. If the Program does not specify a version number of
+ this License, you may choose any version ever published by the Free
+ Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+ whose distribution conditions are different, write to the author to ask
+ for permission. For software which is copyrighted by the Free Software
+ Foundation, write to the Free Software Foundation; we sometimes make
+ exceptions for this. Our decision will be guided by the two goals of
+ preserving the free status of all derivatives of our free software and
+ of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+ THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+ EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH
+ YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+ NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
+ DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
+ DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+ (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+ INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
+ THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR
+ OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
View
95 History.txt
@@ -0,0 +1,95 @@
+=== Net::LDAP 0.0.5 / 2008-11-xx
+
+* 13 minor enhancements:
+ * Added Net::LDAP::Entry#to_ldif
+ * Supported rootDSE searches with a new API.
+ * Added [preliminary (still undocumented) support for SASL authentication.
+ * Supported several constructs from the server side of the LDAP protocol.
+ * Added a "consuming" String#read_ber! method.
+ * Added some support for SNMP data-handling.
+ * Belatedly added a patch contributed by Kouhei Sutou last October.
+ The patch adds start_tls support.
+ * Added Net::LDAP#search_subschema_entry
+ * Added Net::LDAP::Filter#parse_ber, which constructs Net::LDAP::Filter
+ objects directly from BER objects that represent search filters in
+ LDAP SearchRequest packets.
+ * Added Net::LDAP::Filter#execute, which allows arbitrary processing
+ based on LDAP filters.
+ * Changed Net::LDAP::Entry so it can be marshalled and unmarshalled.
+ Thanks to an anonymous feature requester who only left the name
+ "Jammy."
+ * Added support for binary values in Net::LDAP::Entry LDIF conversions
+ and marshalling.
+ * Migrated to 'hoe' as the new project droid.
+
+* 13 bugs fixed:
+ * Silenced some annoying warnings in filter.rb. Thanks to "barjunk"
+ for pointing this out.
+ * Some fairly extensive performance optimizations in the BER parser.
+ * Fixed a bug in Net::LDAP::Entry::from_single_ldif_string noticed by
+ Matthias Tarasiewicz.
+ * Removed an erroneous LdapError value, noticed by Kouhei Sutou.
+ * Supported attributes containing blanks (cn=Babs Jensen) to
+ Filter#construct. Suggested by an anonymous Rubyforge user.
+ * Added missing syntactic support for Filter ANDs, NOTs and a few other
+ things.
+ * Extended support for server-reported error messages. This was provisionally
+ added to Net::LDAP#add, and eventually will be added to other methods.
+ * Fixed bug in Net::LDAP#bind. We were ignoring the passed-in auth parm.
+ Thanks to Kouhei Sutou for spotting it.
+ * Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou
+ for the patch.
+ * Applied an additional patch from Kouhei.
+ * Allowed comma in filter strings, suggested by Kouhei.
+ * 04Sep07, Changed four error classes to inherit from StandardError rather
+ Exception, in order to be friendlier to irb. Suggested by Kouhei.
+ * Minor bug fixes here and there
+
+=== Net::LDAP 0.0.4 / 2006-08-15
+
+* Undeprecated Net::LDAP#modify. Thanks to Justin Forder for
+ providing the rationale for this.
+* Added a much-expanded set of special characters to the parser
+ for RFC-2254 filters. Thanks to Andre Nathan.
+* Changed Net::LDAP#search so you can pass it a filter in string form.
+ The conversion to a Net::LDAP::Filter now happens automatically.
+* Implemented Net::LDAP#bind_as (preliminary and subject to change).
+ Thanks for Simon Claret for valuable suggestions and for helping test.
+* Fixed bug in Net::LDAP#open that was preventing #open from being
+ called more than one on a given Net::LDAP object.
+
+=== Net::LDAP 0.0.3 / 2006-07-26
+
+* Added simple TLS encryption.
+ Thanks to Garett Shulman for suggestions and for helping test.
+
+=== Net::LDAP 0.0.2 / 2006-07-12
+
+* Fixed malformation in distro tarball and gem.
+* Improved documentation.
+* Supported "paged search control."
+* Added a range of API improvements.
+* Thanks to Andre Nathan, andre@digirati.com.br, for valuable
+ suggestions.
+* Added support for LE and GE search filters.
+* Added support for Search referrals.
+* Fixed a regression with openldap 2.2.x and higher caused
+ by the introduction of RFC-2696 controls. Thanks to Andre
+ Nathan for reporting the problem.
+* Added support for RFC-2254 filter syntax.
+
+=== Net::LDAP 0.0.1 / 2006-05-01
+
+* Initial release.
+* Client functionality is near-complete, although the APIs
+ are not guaranteed and may change depending on feedback
+ from the community.
+* We're internally working on a Ruby-based implementation
+ of a full-featured, production-quality LDAP server,
+ which will leverage the underlying LDAP and BER functionality
+ in Net::LDAP.
+* Please tell us if you would be interested in seeing a public
+ release of the LDAP server.
+* Grateful acknowledgement to Austin Ziegler, who reviewed
+ this code and provided the release framework, including
+ minitar.
View
55 LICENSE
@@ -0,0 +1,55 @@
+Net::LDAP is copyrighted free software by Francis Cianfrocca
+<garbagecat10@gmail.com>. You can redistribute it and/or modify it under either
+the terms of the GPL (see the file COPYING), or the conditions below:
+
+1. You may make and give away verbatim copies of the source form of the
+ software without restriction, provided that you duplicate all of the
+ original copyright notices and associated disclaimers.
+
+2. You may modify your copy of the software in any way, provided that you do
+ at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise make them
+ Freely Available, such as by posting said modifications to Usenet or
+ an equivalent medium, or by allowing the author to include your
+ modifications in the software.
+
+ b) use the modified software only within your corporation or
+ organization.
+
+ c) rename any non-standard executables so the names do not conflict with
+ standard executables, which must also be provided.
+
+ d) make other distribution arrangements with the author.
+
+3. You may distribute the software in object code or executable form,
+ provided that you do at least ONE of the following:
+
+ a) distribute the executables and library files of the software, together
+ with instructions (in the manual page or equivalent) on where to get
+ the original distribution.
+
+ b) accompany the distribution with the machine-readable source of the
+ software.
+
+ c) give non-standard executables non-standard names, with instructions on
+ where to get the original software distribution.
+
+ d) make other distribution arrangements with the author.
+
+4. You may modify and include the part of the software into any other
+ software (possibly commercial). But some files in the distribution are
+ not written by the author, so that they are not under this terms.
+
+ They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some
+ files under the ./missing directory. See each file for the copying
+ condition.
+
+5. The scripts and library files supplied as input to or produced as output
+ from the software do not automatically fall under the copyright of the
+ software, but belong to whomever generated them, and may be sold
+ commercially, and may be aggregated with this software.
+
+6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
+ WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
View
37 Manifest.txt
@@ -0,0 +1,37 @@
+COPYING
+History.txt
+LICENSE
+Manifest.txt
+README.txt
+Rakefile
+Release-Announcement
+lib/net/ber.rb
+lib/net/ldap.rb
+lib/net/ldap/dataset.rb
+lib/net/ldap/entry.rb
+lib/net/ldap/filter.rb
+lib/net/ldap/pdu.rb
+lib/net/ldap/psw.rb
+lib/net/ldif.rb
+lib/net/snmp.rb
+pre-setup.rb
+setup.rb
+test/common.rb
+test/test_ber.rb
+test/test_entry.rb
+test/test_filter.rb
+test/test_ldif.rb
+test/test_password.rb
+test/test_snmp.rb
+test/testdata.ldif
+tests/NOTICE.txt
+tests/testber.rb
+tests/testdata.ldif
+tests/testem.rb
+tests/testfilter.rb
+tests/testldap.rb
+tests/testldif.rb
+tests/testpsw.rb
+tests/testsnmp.rb
+testserver/ldapserver.rb
+testserver/testdata.ldif
View
62 README.txt
@@ -0,0 +1,62 @@
+= Net::LDAP for Ruby
+
+* http://rubyforge.org/projects/net-ldap
+
+== DESCRIPTION:
+
+Pure Ruby LDAP library.
+
+== FEATURES/PROBLEMS:
+
+The Lightweight Directory Access Protocol (LDAP) is an Internet protocol
+for accessing distributed directory services.
+
+Net::LDAP is an LDAP support library written in pure Ruby. It supports
+most LDAP client features and a subset of server features as well.
+
+* Standards-based (going for RFC 4511)
+* Portable: 100% Ruby
+
+== SYNOPSIS:
+
+See Net::LDAP for documentation and usage samples.
+
+== REQUIREMENTS:
+
+Net::LDAP requires Ruby 1.8.2 or better.
+
+== INSTALL:
+
+Net::LDAP is a pure Ruby library. It does not require any external
+libraries.
+
+You can install the RubyGems version of Net::LDAP available from the
+usual sources.
+
+* gem install net-ldap
+
+If using the packaged (.tgz) version; it can be installed with:
+
+* ruby setup.rb
+
+== CREDITS:
+
+Net::LDAP was originally developed by:
+
+* Francis Cianfrocca <garbagecat10@gmail.com>
+
+Contributions since:
+
+* Austin Ziegler <halostatue@gmail.com>
+* Emiel van de Laar <gemiel@gmail.com>
+
+== LICENSE:
+
+Copyright (C) 2006 by Francis Cianfrocca
+
+Please read the file LICENSE for licensing restrictions on this library. In
+the simplest terms, this library is available under the same terms as Ruby
+itself.
+
+Available under the same terms as Ruby. See LICENSE in the main
+distribution for full licensing information.
View
18 Rakefile
@@ -0,0 +1,18 @@
+# -*- ruby -*-
+
+require 'rubygems'
+require 'hoe'
+
+# Add 'lib' to load path.
+$LOAD_PATH.unshift( "#{File.dirname(__FILE__)}/lib" )
+
+# Pull in local 'net/ldap' as opposed to an installed version.
+require 'net/ldap'
+
+Hoe.new('net-ldap', Net::LDAP::VERSION) do |p|
+ p.rubyforge_name = 'net-ldap'
+ p.developer('Francis Cianfrocca', 'garbagecat10@gmail.com')
+ p.developer('Emiel van de Laar', 'gemiel@gmail.com')
+end
+
+# vim: syntax=Ruby
View
95 Release-Announcement
@@ -0,0 +1,95 @@
+We're pleased to announce version 0.0.4 of Net::LDAP, the pure-Ruby LDAP library.
+
+This version adds an implementation of Net::LDAP#bind_as, which allows
+you to authenticate users who log into your applications using simple
+identifiers like email addresses and simple usernames. Thanks to Simon Claret
+for suggesting the original implementation in his page on the Rails-Wiki,
+and for valuable comments and help with testing.
+
+We have un-deprecated Net::LDAP#modify, which can be useful with
+LDAP servers that observe non-standard transactional and concurrency
+semantics in LDAP Modify operations. Note: this is a documentation change,
+not a code change. Thanks to Justin Forder for providing the rationale
+for this change.
+
+We added a larger set of special characters which may appear in RFC-2254
+standard search filters. Thanks to Andre Nathan for this patch.
+
+We fixed a bug that was preventing Net::LDAP#open from being called more
+than once on the same object.
+
+
+Net::LDAP is a feature-complete LDAP client which can access as much as
+possible of the functionality of the most-used LDAP server implementations.
+This library does not wrap any existing native-code LDAP libraries, creates no
+Ruby extensions, and has no dependencies external to Ruby.
+
+If anyone wants to contribute suggestions, insights or (especially)
+code, please email me at garbagecat10 .. .. gmail.com.
+
+= What is Net::LDAP for Ruby?
+This library provides a pure-Ruby implementation of an LDAP client.
+It can be used to access any server which implements the LDAP protocol.
+
+Net::LDAP is intended to provide full LDAP functionality while hiding
+the more arcane aspects of the LDAP protocol itself, so as to make the
+programming interface as Ruby-like as possible.
+
+In particular, this means that there is no direct dependence on the
+structure of the various "traditional" LDAP clients. This is a ground-up
+rethinking of the LDAP API.
+
+Net::LDAP is based on RFC-2251, which specifies the Lightweight Directory
+Access Protocol, as amended and extended by subsequent RFCs and by the more
+widely-used directory implementations.
+
+Homepage:: http://rubyforge.org/projects/net-ldap/
+Download:: http://rubyforge.org/frs/?group_id=143
+Copyright:: 2006 by Francis Cianfrocca
+
+== LICENCE NOTES
+Please read the file LICENCE for licensing restrictions on this library. In
+the simplest terms, this library is available under the same terms as Ruby
+itself.
+
+== Requirements and Installation
+Net::LDAP requires Ruby 1.8.2 or better.
+
+Net::LDAP can be installed with:
+
+ % ruby setup.rb
+
+Alternatively, you can use the RubyGems version of Net::LDAP available
+as ruby-net-ldap-0.0.2.gem from the usual sources.
+
+== Whet your appetite:
+ require 'net/ldap'
+
+ ldap = Net::LDAP.new :host => server_ip_address,
+ :port => 389,
+ :auth => {
+ :method => :simple,
+ :username => "cn=manager,dc=example,dc=com",
+ :password => "opensesame"
+ }
+
+ filter = Net::LDAP::Filter.eq( "cn", "George*" )
+ treebase = "dc=example,dc=com"
+
+ ldap.search( :base => treebase, :filter => filter ) do |entry|
+ puts "DN: #{entry.dn}"
+ entry.each do |attribute, values|
+ puts " #{attribute}:"
+ values.each do |value|
+ puts " --->#{value}"
+ end
+ end
+ end
+
+ p ldap.get_operation_result
+
+== Net::LDAP 0.0.2: May 3, 2006
+* Fixed malformation in distro tarball and gem.
+* Improved documentation.
+* Supported "paged search control."
+
View
556 lib/net/ber.rb
@@ -0,0 +1,556 @@
+# $Id$
+#
+# NET::BER
+# Mixes ASN.1/BER convenience methods into several standard classes.
+# Also provides BER parsing functionality.
+#
+#----------------------------------------------------------------------------
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#---------------------------------------------------------------------------
+#
+#
+
+
+module Net
+
+ module BER
+
+ class BerError < StandardError; end
+
+
+ class BerIdentifiedString < String
+ attr_accessor :ber_identifier
+ def initialize args
+ super args
+ end
+ end
+
+ class BerIdentifiedArray < Array
+ attr_accessor :ber_identifier
+ def initialize
+ super
+ end
+ end
+
+ class BerIdentifiedNull
+ attr_accessor :ber_identifier
+ def to_ber
+ "\005\000"
+ end
+ end
+
+ class BerIdentifiedOid
+ attr_accessor :ber_identifier
+ def initialize oid
+ if oid.is_a?(String)
+ oid = oid.split(/\./).map {|s| s.to_i }
+ end
+ @value = oid
+ end
+ def to_ber
+ # Provisional implementation.
+ # We ASSUME that our incoming value is an array, and we
+ # use the Array#to_ber_oid method defined below.
+ # We probably should obsolete that method, actually, in
+ # and move the code here.
+ # WE ARE NOT CURRENTLY ENCODING THE BER-IDENTIFIER.
+ # This implementation currently hardcodes 6, the universal OID tag.
+ ary = @value.dup
+ first = ary.shift
+ raise Net::BER::BerError.new(" invalid OID" ) unless [0,1,2].include?(first)
+ first = first * 40 + ary.shift
+ ary.unshift first
+ oid = ary.pack("w*")
+ [6, oid.length].pack("CC") + oid
+ end
+ end
+
+ #--
+ # This condenses our nicely self-documenting ASN hashes down
+ # to an array for fast lookups.
+ # Scoped to be called as a module method, but not intended for
+ # user code to call.
+ #
+ def self.compile_syntax syn
+ out = [nil] * 256
+ syn.each {|tclass,tclasses|
+ tagclass = {:universal=>0, :application=>64, :context_specific=>128, :private=>192} [tclass]
+ tclasses.each {|codingtype,codings|
+ encoding = {:primitive=>0, :constructed=>32} [codingtype]
+ codings.each {|tag,objtype|
+ out[tagclass + encoding + tag] = objtype
+ }
+ }
+ }
+ out
+ end
+
+ # This module is for mixing into IO and IO-like objects.
+ module BERParser
+
+ # The order of these follows the class-codes in BER.
+ # Maybe this should have been a hash.
+ TagClasses = [:universal, :application, :context_specific, :private]
+
+ BuiltinSyntax = BER.compile_syntax( {
+ :universal => {
+ :primitive => {
+ 1 => :boolean,
+ 2 => :integer,
+ 4 => :string,
+ 5 => :null,
+ 6 => :oid,
+ 10 => :integer,
+ 13 => :string # (relative OID)
+ },
+ :constructed => {
+ 16 => :array,
+ 17 => :array
+ }
+ },
+ :context_specific => {
+ :primitive => {
+ 10 => :integer
+ }
+ }
+ })
+
+ #
+ # read_ber
+ # TODO: clean this up so it works properly with partial
+ # packets coming from streams that don't block when
+ # we ask for more data (like StringIOs). At it is,
+ # this can throw TypeErrors and other nasties.
+ #--
+ # BEWARE, this violates DRY and is largely equal in functionality to
+ # read_ber_from_string. Eventually that method may subsume the functionality
+ # of this one.
+ #
+ def read_ber syntax=nil
+ # don't bother with this line, since IO#getc by definition returns nil on eof.
+ #return nil if eof?
+
+ id = getc or return nil # don't trash this value, we'll use it later
+ #tag = id & 31
+ #tag < 31 or raise BerError.new( "unsupported tag encoding: #{id}" )
+ #tagclass = TagClasses[ id >> 6 ]
+ #encoding = (id & 0x20 != 0) ? :constructed : :primitive
+
+ n = getc
+ lengthlength,contentlength = if n <= 127
+ [1,n]
+ else
+ # Replaced the inject because it profiles hot.
+ #j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc}
+ j = 0
+ read( n & 127 ).each_byte {|n1| j = (j << 8) + n1}
+ [1 + (n & 127), j]
+ end
+
+ newobj = read contentlength
+
+ # This exceptionally clever and clear bit of code is verrrry slow.
+ objtype = (syntax && syntax[id]) || BuiltinSyntax[id]
+
+
+ # == is expensive so sort this if/else so the common cases are at the top.
+ obj = if objtype == :string
+ #(newobj || "").dup
+ s = BerIdentifiedString.new( newobj || "" )
+ s.ber_identifier = id
+ s
+ elsif objtype == :integer
+ j = 0
+ newobj.each_byte {|b| j = (j << 8) + b}
+ j
+ elsif objtype == :oid
+ # cf X.690 pgh 8.19 for an explanation of this algorithm.
+ # Potentially not good enough. We may need a BerIdentifiedOid
+ # as a subclass of BerIdentifiedArray, to get the ber identifier
+ # and also a to_s method that produces the familiar dotted notation.
+ oid = newobj.unpack("w*")
+ f = oid.shift
+ g = if f < 40
+ [0, f]
+ elsif f < 80
+ [1, f-40]
+ else
+ [2, f-80] # f-80 can easily be > 80. What a weird optimization.
+ end
+ oid.unshift g.last
+ oid.unshift g.first
+ oid
+ elsif objtype == :array
+ #seq = []
+ seq = BerIdentifiedArray.new
+ seq.ber_identifier = id
+ sio = StringIO.new( newobj || "" )
+ # Interpret the subobject, but note how the loop
+ # is built: nil ends the loop, but false (a valid
+ # BER value) does not!
+ while (e = sio.read_ber(syntax)) != nil
+ seq << e
+ end
+ seq
+ elsif objtype == :boolean
+ newobj != "\000"
+ elsif objtype == :null
+ n = BerIdentifiedNull.new
+ n.ber_identifier = id
+ n
+ else
+ #raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" )
+ raise BerError.new( "unsupported object type: id=#{id}" )
+ end
+
+ # Add the identifier bits into the object if it's a String or an Array.
+ # We can't add extra stuff to Fixnums and booleans, not that it makes much sense anyway.
+ # Replaced this mechanism with subclasses because the instance_eval profiled too hot.
+ #obj and ([String,Array].include? obj.class) and obj.instance_eval "def ber_identifier; #{id}; end"
+ #obj.ber_identifier = id if obj.respond_to?(:ber_identifier)
+ obj
+
+ end
+
+ #--
+ # Violates DRY! This replicates the functionality of #read_ber.
+ # Eventually this method may replace that one.
+ # This version of #read_ber behaves properly in the face of incomplete
+ # data packets. If a full BER object is detected, we return an array containing
+ # the detected object and the number of bytes consumed from the string.
+ # If we don't detect a complete packet, return nil.
+ #
+ # Observe that weirdly we recursively call the original #read_ber in here.
+ # That needs to be fixed if we ever obsolete the original method in favor of this one.
+ def read_ber_from_string str, syntax=nil
+ id = str[0] or return nil
+ n = str[1] or return nil
+ n_consumed = 2
+ lengthlength,contentlength = if n <= 127
+ [1,n]
+ else
+ n1 = n & 127
+ return nil unless str.length >= (n_consumed + n1)
+ j = 0
+ n1.times {
+ j = (j << 8) + str[n_consumed]
+ n_consumed += 1
+ }
+ [1 + (n1), j]
+ end
+
+ return nil unless str.length >= (n_consumed + contentlength)
+ newobj = str[n_consumed...(n_consumed + contentlength)]
+ n_consumed += contentlength
+
+ objtype = (syntax && syntax[id]) || BuiltinSyntax[id]
+
+ # == is expensive so sort this if/else so the common cases are at the top.
+ obj = if objtype == :array
+ seq = BerIdentifiedArray.new
+ seq.ber_identifier = id
+ sio = StringIO.new( newobj || "" )
+ # Interpret the subobject, but note how the loop
+ # is built: nil ends the loop, but false (a valid
+ # BER value) does not!
+ # Also, we can use the standard read_ber method because
+ # we know for sure we have enough data. (Although this
+ # might be faster than the standard method.)
+ while (e = sio.read_ber(syntax)) != nil
+ seq << e
+ end
+ seq
+ elsif objtype == :string
+ s = BerIdentifiedString.new( newobj || "" )
+ s.ber_identifier = id
+ s
+ elsif objtype == :integer
+ j = 0
+ newobj.each_byte {|b| j = (j << 8) + b}
+ j
+ elsif objtype == :oid
+ # cf X.690 pgh 8.19 for an explanation of this algorithm.
+ # Potentially not good enough. We may need a BerIdentifiedOid
+ # as a subclass of BerIdentifiedArray, to get the ber identifier
+ # and also a to_s method that produces the familiar dotted notation.
+ oid = newobj.unpack("w*")
+ f = oid.shift
+ g = if f < 40
+ [0,f]
+ elsif f < 80
+ [1, f-40]
+ else
+ [2, f-80] # f-80 can easily be > 80. What a weird optimization.
+ end
+ oid.unshift g.last
+ oid.unshift g.first
+ oid
+ elsif objtype == :boolean
+ newobj != "\000"
+ elsif objtype == :null
+ n = BerIdentifiedNull.new
+ n.ber_identifier = id
+ n
+ else
+ raise BerError.new( "unsupported object type: id=#{id}" )
+ end
+
+ [obj, n_consumed]
+ end
+
+ end # module BERParser
+ end # module BER
+
+end # module Net
+
+
+class IO
+ include Net::BER::BERParser
+end
+
+require "stringio"
+class StringIO
+ include Net::BER::BERParser
+end
+
+begin
+ require 'openssl'
+ class OpenSSL::SSL::SSLSocket
+ include Net::BER::BERParser
+ end
+rescue LoadError
+# Ignore LoadError.
+# DON'T ignore NameError, which means the SSLSocket class
+# is somehow unavailable on this implementation of Ruby's openssl.
+# This may be WRONG, however, because we don't yet know how Ruby's
+# openssl behaves on machines with no OpenSSL library. I suppose
+# it's possible they do not fail to require 'openssl' but do not
+# create the classes. So this code is provisional.
+# Also, you might think that OpenSSL::SSL::SSLSocket inherits from
+# IO so we'd pick it up above. But you'd be wrong.
+end
+
+
+
+class String
+ include Net::BER::BERParser
+ def read_ber syntax=nil
+ StringIO.new(self).read_ber(syntax)
+ end
+ def read_ber! syntax=nil
+ obj,n_consumed = read_ber_from_string(self, syntax)
+ if n_consumed
+ self.slice!(0...n_consumed)
+ obj
+ else
+ nil
+ end
+ end
+end
+
+#----------------------------------------------
+
+
+class FalseClass
+ #
+ # to_ber
+ #
+ def to_ber
+ "\001\001\000"
+ end
+end
+
+
+class TrueClass
+ #
+ # to_ber
+ #
+ def to_ber
+ "\001\001\001"
+ end
+end
+
+
+
+class Fixnum
+ #
+ # to_ber
+ #
+ def to_ber
+ "\002" + to_ber_internal
+ end
+
+ #
+ # to_ber_enumerated
+ #
+ def to_ber_enumerated
+ "\012" + to_ber_internal
+ end
+
+ #
+ # to_ber_length_encoding
+ #
+ def to_ber_length_encoding
+ if self <= 127
+ [self].pack('C')
+ else
+ i = [self].pack('N').sub(/^[\0]+/,"")
+ [0x80 + i.length].pack('C') + i
+ end
+ end
+
+ # Generate a BER-encoding for an application-defined INTEGER.
+ # Example: SNMP's Counter, Gauge, and TimeTick types.
+ #
+ def to_ber_application tag
+ [0x40 + tag].pack("C") + to_ber_internal
+ end
+
+ #--
+ # Called internally to BER-encode the length and content bytes of a Fixnum.
+ # The caller will prepend the tag byte.
+ def to_ber_internal
+ # PLEASE optimize this code path. It's awfully ugly and probably slow.
+ # It also doesn't understand negative numbers yet.
+ raise Net::BER::BerError.new( "range error in fixnum" ) unless self >= 0
+ z = [self].pack("N")
+ zlen = if self < 0x80
+ 1
+ elsif self < 0x8000
+ 2
+ elsif self < 0x800000
+ 3
+ else
+ 4
+ end
+ [zlen].pack("C") + z[0-zlen,zlen]
+ end
+ private :to_ber_internal
+
+end # class Fixnum
+
+
+class Bignum
+
+ def to_ber
+ #i = [self].pack('w')
+ #i.length > 126 and raise Net::BER::BerError.new( "range error in bignum" )
+ #[2, i.length].pack("CC") + i
+
+ # Ruby represents Bignums as two's-complement numbers so we may actually be
+ # good as far as representing negatives goes.
+ # I'm sure this implementation can be improved performance-wise if necessary.
+ # Ruby's Bignum#size returns the number of bytes in the internal representation
+ # of the number, but it can and will include leading zero bytes on at least
+ # some implementations. Evidently Ruby stores these as sets of quadbytes.
+ # It's not illegal in BER to encode all of the leading zeroes but let's strip
+ # them out anyway.
+ #
+ sz = self.size
+ out = "\000" * sz
+ (sz*8).times {|bit|
+ if self[bit] == 1
+ out[bit/8] += (1 << (bit % 8))
+ end
+ }
+
+ while out.length > 1 and out[-1] == 0
+ out.slice!(-1,1)
+ end
+
+ [2, out.length].pack("CC") + out.reverse
+ end
+
+end
+
+
+
+class String
+ #
+ # to_ber
+ # A universal octet-string is tag number 4,
+ # but others are possible depending on the context, so we
+ # let the caller give us one.
+ # The preferred way to do this in user code is via to_ber_application_sring
+ # and to_ber_contextspecific.
+ #
+ def to_ber code = 4
+ [code].pack('C') + length.to_ber_length_encoding + self
+ end
+
+ #
+ # to_ber_application_string
+ #
+ def to_ber_application_string code
+ to_ber( 0x40 + code )
+ end
+
+ #
+ # to_ber_contextspecific
+ #
+ def to_ber_contextspecific code
+ to_ber( 0x80 + code )
+ end
+
+end # class String
+
+
+
+class Array
+ #
+ # to_ber_appsequence
+ # An application-specific sequence usually gets assigned
+ # a tag that is meaningful to the particular protocol being used.
+ # This is different from the universal sequence, which usually
+ # gets a tag value of 16.
+ # Now here's an interesting thing: We're adding the X.690
+ # "application constructed" code at the top of the tag byte (0x60),
+ # but some clients, notably ldapsearch, send "context-specific
+ # constructed" (0xA0). The latter would appear to violate RFC-1777,
+ # but what do I know? We may need to change this.
+ #
+
+ def to_ber id = 0; to_ber_seq_internal( 0x30 + id ); end
+ def to_ber_set id = 0; to_ber_seq_internal( 0x31 + id ); end
+ def to_ber_sequence id = 0; to_ber_seq_internal( 0x30 + id ); end
+ def to_ber_appsequence id = 0; to_ber_seq_internal( 0x60 + id ); end
+ def to_ber_contextspecific id = 0; to_ber_seq_internal( 0xA0 + id ); end
+
+ def to_ber_oid
+ ary = self.dup
+ first = ary.shift
+ raise Net::BER::BerError.new( "invalid OID" ) unless [0,1,2].include?(first)
+ first = first * 40 + ary.shift
+ ary.unshift first
+ oid = ary.pack("w*")
+ [6, oid.length].pack("CC") + oid
+ end
+
+ private
+ def to_ber_seq_internal code
+ s = self.to_s
+ [code].pack('C') + s.length.to_ber_length_encoding + s
+ end
+
+
+end # class Array
+
+
View
1,592 lib/net/ldap.rb
@@ -0,0 +1,1592 @@
+# $Id$
+#
+# Net::LDAP for Ruby
+#
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Written and maintained by Francis Cianfrocca, gmail: garbagecat10.
+#
+# This program is free software.
+# You may re-distribute and/or modify this program under the same terms
+# as Ruby itself: Ruby Distribution License or GNU General Public License.
+#
+#
+# See Net::LDAP for documentation and usage samples.
+#
+
+
+require 'socket'
+require 'ostruct'
+
+begin
+ require 'openssl'
+ $net_ldap_openssl_available = true
+rescue LoadError
+end
+
+require 'net/ber'
+require 'net/ldap/pdu'
+require 'net/ldap/filter'
+require 'net/ldap/dataset'
+require 'net/ldap/psw'
+require 'net/ldap/entry'
+
+
+module Net
+
+
+ # == Net::LDAP
+ #
+ # This library provides a pure-Ruby implementation of the
+ # LDAP client protocol, per RFC-2251.
+ # It can be used to access any server which implements the
+ # LDAP protocol.
+ #
+ # Net::LDAP is intended to provide full LDAP functionality
+ # while hiding the more arcane aspects
+ # the LDAP protocol itself, and thus presenting as Ruby-like
+ # a programming interface as possible.
+ #
+ # == Quick-start for the Impatient
+ # === Quick Example of a user-authentication against an LDAP directory:
+ #
+ # require 'rubygems'
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = your_server_ip_address
+ # ldap.port = 389
+ # ldap.auth "joe_user", "opensesame"
+ # if ldap.bind
+ # # authentication succeeded
+ # else
+ # # authentication failed
+ # end
+ #
+ #
+ # === Quick Example of a search against an LDAP directory:
+ #
+ # require 'rubygems'
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new :host => server_ip_address,
+ # :port => 389,
+ # :auth => {
+ # :method => :simple,
+ # :username => "cn=manager,dc=example,dc=com",
+ # :password => "opensesame"
+ # }
+ #
+ # filter = Net::LDAP::Filter.eq( "cn", "George*" )
+ # treebase = "dc=example,dc=com"
+ #
+ # ldap.search( :base => treebase, :filter => filter ) do |entry|
+ # puts "DN: #{entry.dn}"
+ # entry.each do |attribute, values|
+ # puts " #{attribute}:"
+ # values.each do |value|
+ # puts " --->#{value}"
+ # end
+ # end
+ # end
+ #
+ # p ldap.get_operation_result
+ #
+ #
+ # == A Brief Introduction to LDAP
+ #
+ # We're going to provide a quick, informal introduction to LDAP
+ # terminology and
+ # typical operations. If you're comfortable with this material, skip
+ # ahead to "How to use Net::LDAP." If you want a more rigorous treatment
+ # of this material, we recommend you start with the various IETF and ITU
+ # standards that relate to LDAP.
+ #
+ # === Entities
+ # LDAP is an Internet-standard protocol used to access directory servers.
+ # The basic search unit is the <i>entity,</i> which corresponds to
+ # a person or other domain-specific object.
+ # A directory service which supports the LDAP protocol typically
+ # stores information about a number of entities.
+ #
+ # === Principals
+ # LDAP servers are typically used to access information about people,
+ # but also very often about such items as printers, computers, and other
+ # resources. To reflect this, LDAP uses the term <i>entity,</i> or less
+ # commonly, <i>principal,</i> to denote its basic data-storage unit.
+ #
+ #
+ # === Distinguished Names
+ # In LDAP's view of the world,
+ # an entity is uniquely identified by a globally-unique text string
+ # called a <i>Distinguished Name,</i> originally defined in the X.400
+ # standards from which LDAP is ultimately derived.
+ # Much like a DNS hostname, a DN is a "flattened" text representation
+ # of a string of tree nodes. Also like DNS (and unlike Java package
+ # names), a DN expresses a chain of tree-nodes written from left to right
+ # in order from the most-resolved node to the most-general one.
+ #
+ # If you know the DN of a person or other entity, then you can query
+ # an LDAP-enabled directory for information (attributes) about the entity.
+ # Alternatively, you can query the directory for a list of DNs matching
+ # a set of criteria that you supply.
+ #
+ # === Attributes
+ #
+ # In the LDAP view of the world, a DN uniquely identifies an entity.
+ # Information about the entity is stored as a set of <i>Attributes.</i>
+ # An attribute is a text string which is associated with zero or more
+ # values. Most LDAP-enabled directories store a well-standardized
+ # range of attributes, and constrain their values according to standard
+ # rules.
+ #
+ # A good example of an attribute is <tt>sn,</tt> which stands for "Surname."
+ # This attribute is generally used to store a person's surname, or last name.
+ # Most directories enforce the standard convention that
+ # an entity's <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP
+ # jargon, that means that <tt>sn</tt> must be <i>present</i> and
+ # <i>single-valued.</i>
+ #
+ # Another attribute is <tt>mail,</tt> which is used to store email addresses.
+ # (No, there is no attribute called "email," perhaps because X.400 terminology
+ # predates the invention of the term <i>email.</i>) <tt>mail</tt> differs
+ # from <tt>sn</tt> in that most directories permit any number of values for the
+ # <tt>mail</tt> attribute, including zero.
+ #
+ #
+ # === Tree-Base
+ # We said above that X.400 Distinguished Names are <i>globally unique.</i>
+ # In a manner reminiscent of DNS, LDAP supposes that each directory server
+ # contains authoritative attribute data for a set of DNs corresponding
+ # to a specific sub-tree of the (notional) global directory tree.
+ # This subtree is generally configured into a directory server when it is
+ # created. It matters for this discussion because most servers will not
+ # allow you to query them unless you specify a correct tree-base.
+ #
+ # Let's say you work for the engineering department of Big Company, Inc.,
+ # whose internet domain is bigcompany.com. You may find that your departmental
+ # directory is stored in a server with a defined tree-base of
+ # ou=engineering,dc=bigcompany,dc=com
+ # You will need to supply this string as the <i>tree-base</i> when querying this
+ # directory. (Ou is a very old X.400 term meaning "organizational unit."
+ # Dc is a more recent term meaning "domain component.")
+ #
+ # === LDAP Versions
+ # (stub, discuss v2 and v3)
+ #
+ # === LDAP Operations
+ # The essential operations are: #bind, #search, #add, #modify, #delete, and #rename.
+ # ==== Bind
+ # #bind supplies a user's authentication credentials to a server, which in turn verifies
+ # or rejects them. There is a range of possibilities for credentials, but most directories
+ # support a simple username and password authentication.
+ #
+ # Taken by itself, #bind can be used to authenticate a user against information
+ # stored in a directory, for example to permit or deny access to some other resource.
+ # In terms of the other LDAP operations, most directories require a successful #bind to
+ # be performed before the other operations will be permitted. Some servers permit certain
+ # operations to be performed with an "anonymous" binding, meaning that no credentials are
+ # presented by the user. (We're glossing over a lot of platform-specific detail here.)
+ #
+ # ==== Search
+ # Calling #search against the directory involves specifying a treebase, a set of <i>search filters,</i>
+ # and a list of attribute values.
+ # The filters specify ranges of possible values for particular attributes. Multiple
+ # filters can be joined together with AND, OR, and NOT operators.
+ # A server will respond to a #search by returning a list of matching DNs together with a
+ # set of attribute values for each entity, depending on what attributes the search requested.
+ #
+ # ==== Add
+ # #add specifies a new DN and an initial set of attribute values. If the operation
+ # succeeds, a new entity with the corresponding DN and attributes is added to the directory.
+ #
+ # ==== Modify
+ # #modify specifies an entity DN, and a list of attribute operations. #modify is used to change
+ # the attribute values stored in the directory for a particular entity.
+ # #modify may add or delete attributes (which are lists of values) or it change attributes by
+ # adding to or deleting from their values.
+ # Net::LDAP provides three easier methods to modify an entry's attribute values:
+ # #add_attribute, #replace_attribute, and #delete_attribute.
+ #
+ # ==== Delete
+ # #delete specifies an entity DN. If it succeeds, the entity and all its attributes
+ # is removed from the directory.
+ #
+ # ==== Rename (or Modify RDN)
+ # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP protocol. It responds to
+ # the often-arising need to change the DN of an entity without discarding its attribute values.
+ # In earlier LDAP versions, the only way to do this was to delete the whole entity and add it
+ # again with a different DN.
+ #
+ # #rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most
+ # part of the DN string. If successful, #rename changes the entity DN so that its left-most
+ # node corresponds to the new RDN given in the request. (RDN, or "relative distinguished name,"
+ # denotes a single tree-node as expressed in a DN, which is a chain of tree nodes.)
+ #
+ # == How to use Net::LDAP
+ #
+ # To access Net::LDAP functionality in your Ruby programs, start by requiring
+ # the library:
+ #
+ # require 'net/ldap'
+ #
+ # If you installed the Gem version of Net::LDAP, and depending on your version of
+ # Ruby and rubygems, you _may_ also need to require rubygems explicitly:
+ #
+ # require 'rubygems'
+ # require 'net/ldap'
+ #
+ # Most operations with Net::LDAP start by instantiating a Net::LDAP object.
+ # The constructor for this object takes arguments specifying the network location
+ # (address and port) of the LDAP server, and also the binding (authentication)
+ # credentials, typically a username and password.
+ # Given an object of class Net:LDAP, you can then perform LDAP operations by calling
+ # instance methods on the object. These are documented with usage examples below.
+ #
+ # The Net::LDAP library is designed to be very disciplined about how it makes network
+ # connections to servers. This is different from many of the standard native-code
+ # libraries that are provided on most platforms, which share bloodlines with the
+ # original Netscape/Michigan LDAP client implementations. These libraries sought to
+ # insulate user code from the workings of the network. This is a good idea of course,
+ # but the practical effect has been confusing and many difficult bugs have been caused
+ # by the opacity of the native libraries, and their variable behavior across platforms.
+ #
+ # In general, Net::LDAP instance methods which invoke server operations make a connection
+ # to the server when the method is called. They execute the operation (typically binding first)
+ # and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection
+ # to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open
+ # closes the connection on completion of the block.
+ #
+
+ class LDAP
+
+ class LdapError < StandardError; end
+
+ VERSION = "0.0.5"
+
+
+ SearchScope_BaseObject = 0
+ SearchScope_SingleLevel = 1
+ SearchScope_WholeSubtree = 2
+ SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree]
+
+ AsnSyntax = BER.compile_syntax({
+ :application => {
+ :primitive => {
+ 2 => :null # UnbindRequest body
+ },
+ :constructed => {
+ 0 => :array, # BindRequest
+ 1 => :array, # BindResponse
+ 2 => :array, # UnbindRequest
+ 3 => :array, # SearchRequest
+ 4 => :array, # SearchData
+ 5 => :array, # SearchResult
+ 6 => :array, # ModifyRequest
+ 7 => :array, # ModifyResponse
+ 8 => :array, # AddRequest
+ 9 => :array, # AddResponse
+ 10 => :array, # DelRequest
+ 11 => :array, # DelResponse
+ 12 => :array, # ModifyRdnRequest
+ 13 => :array, # ModifyRdnResponse
+ 14 => :array, # CompareRequest
+ 15 => :array, # CompareResponse
+ 16 => :array, # AbandonRequest
+ 19 => :array, # SearchResultReferral
+ 24 => :array, # Unsolicited Notification
+ }
+ },
+ :context_specific => {
+ :primitive => {
+ 0 => :string, # password
+ 1 => :string, # Kerberos v4
+ 2 => :string, # Kerberos v5
+ 7 => :string, # serverSaslCreds
+ },
+ :constructed => {
+ 0 => :array, # RFC-2251 Control and Filter-AND
+ 1 => :array, # SearchFilter-OR
+ 2 => :array, # SearchFilter-NOT
+ 3 => :array, # Seach referral
+ 4 => :array, # unknown use in Microsoft Outlook
+ 5 => :array, # SearchFilter-GE
+ 6 => :array, # SearchFilter-LE
+ 7 => :array, # serverSaslCreds
+ }
+ }
+ })
+
+ DefaultHost = "127.0.0.1"
+ DefaultPort = 389
+ DefaultAuth = {:method => :anonymous}
+ DefaultTreebase = "dc=com"
+
+ StartTlsOid = "1.3.6.1.4.1.1466.20037"
+
+ ResultStrings = {
+ 0 => "Success",
+ 1 => "Operations Error",
+ 2 => "Protocol Error",
+ 3 => "Time Limit Exceeded",
+ 4 => "Size Limit Exceeded",
+ 12 => "Unavailable crtical extension",
+ 14 => "saslBindInProgress",
+ 16 => "No Such Attribute",
+ 17 => "Undefined Attribute Type",
+ 20 => "Attribute or Value Exists",
+ 32 => "No Such Object",
+ 34 => "Invalid DN Syntax",
+ 48 => "Inappropriate Authentication",
+ 49 => "Invalid Credentials",
+ 50 => "Insufficient Access Rights",
+ 51 => "Busy",
+ 52 => "Unavailable",
+ 53 => "Unwilling to perform",
+ 65 => "Object Class Violation",
+ 68 => "Entry Already Exists"
+ }
+
+
+ module LdapControls
+ PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
+ end
+
+
+ #
+ # LDAP::result2string
+ #
+ def LDAP::result2string code # :nodoc:
+ ResultStrings[code] || "unknown result (#{code})"
+ end
+
+
+ attr_accessor :host, :port, :base
+
+
+ # Instantiate an object of type Net::LDAP to perform directory operations.
+ # 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
+ # are supported:
+ # * :host => the LDAP server's IP-address (default 127.0.0.1)
+ # * :port => the LDAP server's TCP port (default 389)
+ # * :auth => a Hash containing authorization parameters. Currently supported values include:
+ # {:method => :anonymous} and
+ # {:method => :simple, :username => your_user_name, :password => your_password }
+ # The password parameter may be a Proc that returns a String.
+ # * :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.
+ # * :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.
+ #
+ # Instantiating a Net::LDAP object does <i>not</i> result in network traffic to
+ # the LDAP server. It simply stores the connection and binding parameters in the
+ # object.
+ #
+ def initialize args = {}
+ @host = args[:host] || DefaultHost
+ @port = args[:port] || DefaultPort
+ @verbose = false # Make this configurable with a switch on the class.
+ @auth = args[:auth] || DefaultAuth
+ @base = args[:base] || DefaultTreebase
+ encryption args[:encryption] # may be nil
+
+ if pr = @auth[:password] and pr.respond_to?(:call)
+ @auth[:password] = pr.call
+ end
+
+ # This variable is only set when we are created with LDAP::open.
+ # All of our internal methods will connect using it, or else
+ # they will create their own.
+ @open_connection = nil
+ end
+
+ # Convenience method to specify authentication credentials to the LDAP
+ # server. Currently supports simple authentication requiring
+ # a username and password.
+ #
+ # Observe that on most LDAP servers,
+ # the username is a complete DN. However, with A/D, it's often possible
+ # to give only a user-name rather than a complete DN. In the latter
+ # case, beware that many A/D servers are configured to permit anonymous
+ # (uncredentialled) binding, and will silently accept your binding
+ # as anonymous if you give an unrecognized username. This is not usually
+ # what you want. (See #get_operation_result.)
+ #
+ # <b>Important:</b> The password argument may be a Proc that returns a string.
+ # This makes it possible for you to write client programs that solicit
+ # passwords from users or from other data sources without showing them
+ # in your code or on command lines.
+ #
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = server_ip_address
+ # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw"
+ #
+ # Alternatively (with a password block):
+ #
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = server_ip_address
+ # psw = proc { your_psw_function }
+ # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", psw
+ #
+ def authenticate username, password
+ password = password.call if password.respond_to?(:call)
+ @auth = {:method => :simple, :username => username, :password => password}
+ end
+
+ alias_method :auth, :authenticate
+
+ # Convenience method to specify encryption characteristics for connections
+ # to LDAP servers. Called implicitly by #new and #open, but may also be called
+ # by user code if desired.
+ # The single argument is generally a Hash (but see below for convenience alternatives).
+ # This implementation is currently a stub, supporting only a few encryption
+ # alternatives. As additional capabilities are added, more configuration values
+ # will be added here.
+ #
+ # Currently, the only supported argument is {:method => :simple_tls}.
+ # (Equivalently, you may pass the symbol :simple_tls all by itself, without
+ # enclosing it in a Hash.)
+ #
+ # The :simple_tls encryption method encrypts <i>all</i> communications with the LDAP
+ # server.
+ # It completely establishes SSL/TLS encryption with the LDAP server
+ # before any LDAP-protocol data is exchanged.
+ # There is no plaintext negotiation and no special encryption-request controls
+ # are sent to the server.
+ # <i>The :simple_tls option is the simplest, easiest way to encrypt communications
+ # between Net::LDAP and LDAP servers.</i>
+ # It's intended for cases where you have an implicit level of trust in the authenticity
+ # of the LDAP server. No validation of the LDAP server's SSL certificate is
+ # performed. This means that :simple_tls will not produce errors if the LDAP
+ # server's encryption certificate is not signed by a well-known Certification
+ # Authority.
+ # If you get communications or protocol errors when using this option, check
+ # with your LDAP server administrator. Pay particular attention to the TCP port
+ # you are connecting to. It's impossible for an LDAP server to support plaintext
+ # LDAP communications and <i>simple TLS</i> connections on the same port.
+ # The standard TCP port for unencrypted LDAP connections is 389, but the standard
+ # port for simple-TLS encrypted connections is 636. Be sure you are using the
+ # correct port.
+ #
+ # <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP control,
+ # which will enable encrypted communications on the same TCP port used for
+ # unencrypted connections.]</i>
+ #
+ def encryption args
+ case args
+ when :simple_tls, :start_tls
+ args = {:method => args}
+ end
+ @encryption = args
+ end
+
+
+ # #open takes the same parameters as #new. #open makes a network connection to the
+ # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block.
+ # Within the block, you can call any of the instance methods of Net::LDAP to
+ # perform operations against the LDAP directory. #open will perform all the
+ # operations in the user-supplied block on the same network connection, which
+ # will be closed automatically when the block finishes.
+ #
+ # # (PSEUDOCODE)
+ # auth = {:method => :simple, :username => username, :password => password}
+ # Net::LDAP.open( :host => ipaddress, :port => 389, :auth => auth ) do |ldap|
+ # ldap.search( ... )
+ # ldap.add( ... )
+ # ldap.modify( ... )
+ # end
+ #
+ def LDAP::open args
+ ldap1 = LDAP.new args
+ ldap1.open {|ldap| yield ldap }
+ end
+
+ # Returns a meaningful result any time after
+ # a protocol operation (#bind, #search, #add, #modify, #rename, #delete)
+ # has completed.
+ # It returns an #OpenStruct containing an LDAP result code (0 means success),
+ # and a human-readable string.
+ # unless ldap.bind
+ # puts "Result: #{ldap.get_operation_result.code}"
+ # puts "Message: #{ldap.get_operation_result.message}"
+ # end
+ #
+ # Certain operations return additional information, accessible through members
+ # of the object returned from #get_operation_result. Check #get_operation_result.error_message
+ # and #get_operation_result.matched_dn.
+ #
+ #--
+ # Modified the implementation, 20Mar07. We might get a hash of LDAP response codes
+ # instead of a simple numeric code.
+ #
+ def get_operation_result
+ os = OpenStruct.new
+ if @result.is_a?(Hash)
+ os.code = (@result[:resultCode] || "").to_i
+ os.error_message = @result[:errorMessage]
+ os.matched_dn = @result[:matchedDN]
+ elsif @result
+ os.code = @result
+ else
+ os.code = 0
+ end
+ os.message = LDAP.result2string( os.code )
+ os
+ end
+
+
+ # Opens a network connection to the server and then
+ # passes <tt>self</tt> to the caller-supplied block. The connection is
+ # closed when the block completes. Used for executing multiple
+ # LDAP operations without requiring a separate network connection
+ # (and authentication) for each one.
+ # <i>Note:</i> You do not need to log-in or "bind" to the server. This will
+ # be done for you automatically.
+ # For an even simpler approach, see the class method Net::LDAP#open.
+ #
+ # # (PSEUDOCODE)
+ # auth = {:method => :simple, :username => username, :password => password}
+ # ldap = Net::LDAP.new( :host => ipaddress, :port => 389, :auth => auth )
+ # ldap.open do |ldap|
+ # ldap.search( ... )
+ # ldap.add( ... )
+ # ldap.modify( ... )
+ # end
+ #--
+ # First we make a connection and then a binding, but we don't
+ # do anything with the bind results.
+ # We then pass self to the caller's block, where he will execute
+ # his LDAP operations. Of course they will all generate auth failures
+ # if the bind was unsuccessful.
+ def open
+ raise LdapError.new( "open already in progress" ) if @open_connection
+ @open_connection = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ @open_connection.bind @auth
+ yield self
+ @open_connection.close
+ @open_connection = nil
+ end
+
+
+ # Searches the LDAP directory for directory entries.
+ # Takes a hash argument with parameters. Supported parameters include:
+ # * :base (a string specifying the tree-base for the search);
+ # * :filter (an object of type Net::LDAP::Filter, defaults to objectclass=*);
+ # * :attributes (a string or array of strings specifying the LDAP attributes to return from the server);
+ # * :return_result (a boolean specifying whether to return a result set).
+ # * :attributes_only (a boolean flag, defaults false)
+ # * :scope (one of: Net::LDAP::SearchScope_BaseObject, Net::LDAP::SearchScope_SingleLevel, Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
+ # * :size (an integer indicating the maximum number of search entries to return. Default is zero, which signifies no limit.)
+ #
+ # #search queries the LDAP server and passes <i>each entry</i> to the
+ # caller-supplied block, as an object of type Net::LDAP::Entry.
+ # If the search returns 1000 entries, the block will
+ # be called 1000 times. If the search returns no entries, the block will
+ # not be called.
+ #
+ #--
+ # ORIGINAL TEXT, replaced 04May06.
+ # #search returns either a result-set or a boolean, depending on the
+ # value of the <tt>:return_result</tt> argument. The default behavior is to return
+ # a result set, which is a hash. Each key in the hash is a string specifying
+ # the DN of an entry. The corresponding value for each key is a Net::LDAP::Entry object.
+ # If you request a result set and #search fails with an error, it will return nil.
+ # Call #get_operation_result to get the error information returned by
+ # the LDAP server.
+ #++
+ # #search returns either a result-set or a boolean, depending on the
+ # value of the <tt>:return_result</tt> argument. The default behavior is to return
+ # a result set, which is an Array of objects of class Net::LDAP::Entry.
+ # If you request a result set and #search fails with an error, it will return nil.
+ # Call #get_operation_result to get the error information returned by
+ # the LDAP server.
+ #
+ # When <tt>:return_result => false,</tt> #search will
+ # return only a Boolean, to indicate whether the operation succeeded. This can improve performance
+ # with very large result sets, because the library can discard each entry from memory after
+ # your block processes it.
+ #
+ #
+ # treebase = "dc=example,dc=com"
+ # filter = Net::LDAP::Filter.eq( "mail", "a*.com" )
+ # attrs = ["mail", "cn", "sn", "objectclass"]
+ # ldap.search( :base => treebase, :filter => filter, :attributes => attrs, :return_result => false ) do |entry|
+ # puts "DN: #{entry.dn}"
+ # entry.each do |attr, values|
+ # puts ".......#{attr}:"
+ # values.each do |value|
+ # puts " #{value}"
+ # end
+ # end
+ # end
+ #
+ #--
+ # This is a re-implementation of search that replaces the
+ # original one (now renamed searchx and possibly destined to go away).
+ # The difference is that we return a dataset (or nil) from the
+ # call, and pass _each entry_ as it is received from the server
+ # to the caller-supplied block. This will probably make things
+ # far faster as we can do useful work during the network latency
+ # of the search. The downside is that we have no access to the
+ # whole set while processing the blocks, so we can't do stuff
+ # like sort the DNs until after the call completes.
+ # It's also possible that this interacts badly with server timeouts.
+ # We'll have to ensure that something reasonable happens if
+ # the caller has processed half a result set when we throw a timeout
+ # error.
+ # Another important difference is that we return a result set from
+ # this method rather than a T/F indication.
+ # Since this can be very heavy-weight, we define an argument flag
+ # that the caller can set to suppress the return of a result set,
+ # if he's planning to process every entry as it comes from the server.
+ #
+ # REINTERPRETED the result set, 04May06. Originally this was a hash
+ # of entries keyed by DNs. But let's get away from making users
+ # handle DNs. Change it to a plain array. Eventually we may
+ # want to return a Dataset object that delegates to an internal
+ # array, so we can provide sort methods and what-not.
+ #
+ def search args = {}
+ unless args[:ignore_server_caps]
+ args[:paged_searches_supported] = paged_searches_supported?
+ end
+
+ args[:base] ||= @base
+ result_set = (args and args[:return_result] == false) ? nil : []
+
+ if @open_connection
+ @result = @open_connection.search( args ) {|entry|
+ result_set << entry if result_set
+ yield( entry ) if block_given?
+ }
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.search( args ) {|entry|
+ result_set << entry if result_set
+ yield( entry ) if block_given?
+ }
+ end
+ conn.close
+ end
+
+ @result == 0 and result_set
+ end
+
+ # #bind connects to an LDAP server and requests authentication
+ # based on the <tt>:auth</tt> parameter passed to #open or #new.
+ # It takes no parameters.
+ #
+ # User code does not need to call #bind directly. It will be called
+ # implicitly by the library whenever you invoke an LDAP operation,
+ # such as #search or #add.
+ #
+ # It is useful, however, to call #bind in your own code when the
+ # only operation you intend to perform against the directory is
+ # to validate a login credential. #bind returns true or false
+ # to indicate whether the binding was successful. Reasons for
+ # failure include malformed or unrecognized usernames and
+ # incorrect passwords. Use #get_operation_result to find out
+ # what happened in case of failure.
+ #
+ # Here's a typical example using #bind to authenticate a
+ # credential which was (perhaps) solicited from the user of a
+ # web site:
+ #
+ # require 'net/ldap'
+ # ldap = Net::LDAP.new
+ # ldap.host = your_server_ip_address
+ # ldap.port = 389
+ # ldap.auth your_user_name, your_user_password
+ # if ldap.bind
+ # # authentication succeeded
+ # else
+ # # authentication failed
+ # p ldap.get_operation_result
+ # end
+ #
+ # Here's a more succinct example which does exactly the same thing, but
+ # collects all the required parameters into arguments:
+ #
+ # require 'net/ldap'
+ # ldap = Net::LDAP.new( :host=>your_server_ip_address, :port=>389 )
+ # if ldap.bind( :method=>:simple, :username=>your_user_name, :password=>your_user_password )
+ # # authentication succeeded
+ # else
+ # # authentication failed
+ # p ldap.get_operation_result
+ # end
+ #
+ # You don't need to pass a user-password as a String object to bind. You can
+ # also pass a Ruby Proc object which returns a string. This will cause bind to
+ # execute the Proc (which might then solicit input from a user with console display
+ # suppressed). The String value returned from the Proc is used as the password.
+ #
+ # You don't have to create a new instance of Net::LDAP every time
+ # you perform a binding in this way. If you prefer, you can cache the Net::LDAP object
+ # and re-use it to perform subsequent bindings, <i>provided</i> you call
+ # #auth to specify a new credential before calling #bind. Otherwise, you'll
+ # just re-authenticate the previous user! (You don't need to re-set
+ # the values of #host and #port.) As noted in the documentation for #auth,
+ # the password parameter can be a Ruby Proc instead of a String.
+ #
+ #--
+ # If there is an @open_connection, then perform the bind
+ # on it. Otherwise, connect, bind, and disconnect.
+ # The latter operation is obviously useful only as an auth check.
+ #
+ def bind auth=@auth
+ if @open_connection
+ @result = @open_connection.bind auth
+ else
+ conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption)
+ @result = conn.bind auth
+ conn.close
+ end
+
+ @result == 0
+ end
+
+
+ #
+ # #bind_as is for testing authentication credentials.
+ #
+ # As described under #bind, most LDAP servers require that you supply a complete DN
+ # as a binding-credential, along with an authenticator such as a password.
+ # But for many applications (such as authenticating users to a Rails application),
+ # you often don't have a full DN to identify the user. You usually get a simple
+ # identifier like a username or an email address, along with a password.
+ # #bind_as allows you to authenticate these user-identifiers.
+ #
+ # #bind_as is a combination of a search and an LDAP binding. First, it connects and
+ # binds to the directory as normal. Then it searches the directory for an entry
+ # corresponding to the email address, username, or other string that you supply.
+ # If the entry exists, then #bind_as will <b>re-bind</b> as that user with the
+ # password (or other authenticator) that you supply.
+ #
+ # #bind_as takes the same parameters as #search, <i>with the addition of an
+ # authenticator.</i> Currently, this authenticator must be <tt>:password</tt>.
+ # Its value may be either a String, or a +proc+ that returns a String.
+ # #bind_as returns +false+ on failure. On success, it returns a result set,
+ # just as #search does. This result set is an Array of objects of
+ # type Net::LDAP::Entry. It contains the directory attributes corresponding to
+ # the user. (Just test whether the return value is logically true, if you don't
+ # need this additional information.)
+ #
+ # Here's how you would use #bind_as to authenticate an email address and password:
+ #
+ # require 'net/ldap'
+ #
+ # user,psw = "joe_user@yourcompany.com", "joes_psw"
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = "192.168.0.100"
+ # ldap.port = 389
+ # ldap.auth "cn=manager,dc=yourcompany,dc=com", "topsecret"
+ #
+ # result = ldap.bind_as(
+ # :base => "dc=yourcompany,dc=com",
+ # :filter => "(mail=#{user})",
+ # :password => psw
+ # )
+ # if result
+ # puts "Authenticated #{result.first.dn}"
+ # else
+ # puts "Authentication FAILED."
+ # end
+ def bind_as args={}
+ result = false
+ open {|me|
+ rs = search args
+ if rs and rs.first and dn = rs.first.dn
+ password = args[:password]
+ password = password.call if password.respond_to?(:call)
+ result = rs if bind :method => :simple, :username => dn, :password => password
+ end
+ }
+ result
+ end
+
+
+ # Adds a new entry to the remote LDAP server.
+ # Supported arguments:
+ # :dn :: Full DN of the new entry
+ # :attributes :: Attributes of the new entry.
+ #
+ # The attributes argument is supplied as a Hash keyed by Strings or Symbols
+ # giving the attribute name, and mapping to Strings or Arrays of Strings
+ # giving the actual attribute values. Observe that most LDAP directories
+ # enforce schema constraints on the attributes contained in entries.
+ # #add will fail with a server-generated error if your attributes violate
+ # the server-specific constraints.
+ # Here's an example:
+ #
+ # dn = "cn=George Smith,ou=people,dc=example,dc=com"
+ # attr = {
+ # :cn => "George Smith",
+ # :objectclass => ["top", "inetorgperson"],
+ # :sn => "Smith",
+ # :mail => "gsmith@example.com"
+ # }
+ # Net::LDAP.open (:host => host) do |ldap|
+ # ldap.add( :dn => dn, :attributes => attr )
+ # end
+ #--
+ # Provisional modification: Connection#add returns a full hash with LDAP status values,
+ # instead of the simple result number we're used to getting.
+ #
+ def add args
+ if @open_connection
+ @result = @open_connection.add( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption)
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.add( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+
+ # Modifies the attribute values of a particular entry on the LDAP directory.
+ # Takes a hash with arguments. Supported arguments are:
+ # :dn :: (the full DN of the entry whose attributes are to be modified)
+ # :operations :: (the modifications to be performed, detailed next)
+ #
+ # This method returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result.
+ #
+ # Also see #add_attribute, #replace_attribute, or #delete_attribute, which
+ # provide simpler interfaces to this functionality.
+ #
+ # The LDAP protocol provides a full and well thought-out set of operations
+ # for changing the values of attributes, but they are necessarily somewhat complex
+ # and not always intuitive. If these instructions are confusing or incomplete,
+ # please send us email or create a bug report on rubyforge.
+ #
+ # The :operations parameter to #modify takes an array of operation-descriptors.
+ # Each individual operation is specified in one element of the array, and
+ # most LDAP servers will attempt to perform the operations in order.
+ #
+ # Each of the operations appearing in the Array must itself be an Array
+ # with exactly three elements:
+ # an operator:: must be :add, :replace, or :delete
+ # an attribute name:: the attribute name (string or symbol) to modify
+ # a value:: either a string or an array of strings.
+ #
+ # The :add operator will, unsurprisingly, add the specified values to
+ # the specified attribute. If the attribute does not already exist,
+ # :add will create it. Most LDAP servers will generate an error if you
+ # try to add a value that already exists.
+ #
+ # :replace will erase the current value(s) for the specified attribute,
+ # if there are any, and replace them with the specified value(s).
+ #
+ # :delete will remove the specified value(s) from the specified attribute.
+ # If you pass nil, an empty string, or an empty array as the value parameter
+ # to a :delete operation, the _entire_ _attribute_ will be deleted, along
+ # with all of its values.
+ #
+ # For example:
+ #
+ # dn = "mail=modifyme@example.com,ou=people,dc=example,dc=com"
+ # ops = [
+ # [:add, :mail, "aliasaddress@example.com"],
+ # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]],
+ # [:delete, :sn, nil]
+ # ]
+ # ldap.modify :dn => dn, :operations => ops
+ #
+ # <i>(This example is contrived since you probably wouldn't add a mail
+ # value right before replacing the whole attribute, but it shows that order
+ # of execution matters. Also, many LDAP servers won't let you delete SN
+ # because that would be a schema violation.)</i>
+ #
+ # It's essential to keep in mind that if you specify more than one operation in
+ # a call to #modify, most LDAP servers will attempt to perform all of the operations
+ # in the order you gave them.
+ # This matters because you may specify operations on the
+ # same attribute which must be performed in a certain order.
+ #
+ # Most LDAP servers will _stop_ processing your modifications if one of them
+ # causes an error on the server (such as a schema-constraint violation).
+ # If this happens, you will probably get a result code from the server that
+ # reflects only the operation that failed, and you may or may not get extended
+ # information that will tell you which one failed. #modify has no notion
+ # of an atomic transaction. If you specify a chain of modifications in one
+ # call to #modify, and one of them fails, the preceding ones will usually
+ # not be "rolled back," resulting in a partial update. This is a limitation
+ # of the LDAP protocol, not of Net::LDAP.
+ #
+ # The lack of transactional atomicity in LDAP means that you're usually
+ # better off using the convenience methods #add_attribute, #replace_attribute,
+ # and #delete_attribute, which are are wrappers over #modify. However, certain
+ # LDAP servers may provide concurrency semantics, in which the several operations
+ # contained in a single #modify call are not interleaved with other
+ # modification-requests received simultaneously by the server.
+ # It bears repeating that this concurrency does _not_ imply transactional
+ # atomicity, which LDAP does not provide.
+ #
+ def modify args
+ if @open_connection
+ @result = @open_connection.modify( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.modify( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+
+ # Add a value to an attribute.
+ # Takes the full DN of the entry to modify,
+ # the name (Symbol or String) of the attribute, and the value (String or
+ # Array). If the attribute does not exist (and there are no schema violations),
+ # #add_attribute will create it with the caller-specified values.
+ # If the attribute already exists (and there are no schema violations), the
+ # caller-specified values will be _added_ to the values already present.
+ #
+ # Returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result. See also #replace_attribute and #delete_attribute.
+ #
+ # dn = "cn=modifyme,dc=example,dc=com"
+ # ldap.add_attribute dn, :mail, "newmailaddress@example.com"
+ #
+ def add_attribute dn, attribute, value
+ modify :dn => dn, :operations => [[:add, attribute, value]]
+ end
+
+ # Replace the value of an attribute.
+ # #replace_attribute can be thought of as equivalent to calling #delete_attribute
+ # followed by #add_attribute. It takes the full DN of the entry to modify,
+ # the name (Symbol or String) of the attribute, and the value (String or
+ # Array). If the attribute does not exist, it will be created with the
+ # caller-specified value(s). If the attribute does exist, its values will be
+ # _discarded_ and replaced with the caller-specified values.
+ #
+ # Returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result. See also #add_attribute and #delete_attribute.
+ #
+ # dn = "cn=modifyme,dc=example,dc=com"
+ # ldap.replace_attribute dn, :mail, "newmailaddress@example.com"
+ #
+ def replace_attribute dn, attribute, value
+ modify :dn => dn, :operations => [[:replace, attribute, value]]
+ end
+
+ # Delete an attribute and all its values.
+ # Takes the full DN of the entry to modify, and the
+ # name (Symbol or String) of the attribute to delete.
+ #
+ # Returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result. See also #add_attribute and #replace_attribute.
+ #
+ # dn = "cn=modifyme,dc=example,dc=com"
+ # ldap.delete_attribute dn, :mail
+ #
+ def delete_attribute dn, attribute
+ modify :dn => dn, :operations => [[:delete, attribute, nil]]
+ end
+
+
+ # Rename an entry on the remote DIS by changing the last RDN of its DN.
+ # _Documentation_ _stub_
+ #
+ def rename args
+ if @open_connection
+ @result = @open_connection.rename( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.rename( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+ # modify_rdn is an alias for #rename.
+ def modify_rdn args
+ rename args
+ end
+
+ # Delete an entry from the LDAP directory.
+ # Takes a hash of arguments.
+ # The only supported argument is :dn, which must
+ # give the complete DN of the entry to be deleted.
+ # Returns True or False to indicate whether the delete
+ # succeeded. Extended status information is available by
+ # calling #get_operation_result.
+ #
+ # dn = "mail=deleteme@example.com,ou=people,dc=example,dc=com"
+ # ldap.delete :dn => dn
+ #
+ def delete args
+ if @open_connection
+ @result = @open_connection.delete( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.delete( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+
+ # (Experimental, subject to change).
+ # Return the rootDSE record from the LDAP server as a Net::LDAP::Entry, or an
+ # empty Entry if the server doesn't return the record.
+ #--
+ # cf. RFC4512 graf 5.1.
+ # Note that the rootDSE record we return on success has an empty DN, which is correct.
+ # On failure, the empty Entry will have a nil DN. There's no real reason for that,
+ # so it can be changed if desired.
+ # The funky number-disagreements in the set of attribute names is correct per the RFC.
+ # We may be called by #search itself, which may need to determine things like paged
+ # search capabilities. So to avoid an infinite regress, set :ignore_server_caps,
+ # which prevents us getting called recursively.
+ #
+ def search_root_dse
+ rs = search(
+ :ignore_server_caps=>true,
+ :base=>"",
+ :scope=>SearchScope_BaseObject,
+ :attributes=>[:namingContexts,:supportedLdapVersion,:altServer,:supportedControl,:supportedExtension,:supportedFeatures,:supportedSASLMechanisms]
+ )
+ (rs and rs.first) or Entry.new
+ end
+
+
+ # Return the root Subschema record from the LDAP server as a Net::LDAP::Entry,
+ # or an empty Entry if the server doesn't return the record. On success, the
+ # Net::LDAP::Entry returned from this call will have the attributes :dn,
+ # :objectclasses, and :attributetypes. If there is an error, call #get_operation_result
+ # for more information.
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = "your.ldap.host"
+ # ldap.auth "your-user-dn", "your-psw"
+ # subschema_entry = ldap.search_subschema_entry
+ #
+ # subschema_entry.attributetypes.each do |attrtype|
+ # # your code
+ # end
+ #
+ # subschema_entry.objectclasses.each do |attrtype|
+ # # your code
+ # end
+ #--
+ # cf. RFC4512 section 4, particulary graff 4.4.
+ # The :dn attribute in the returned Entry is the subschema name as returned from
+ # the server.
+ # Set :ignore_server_caps, see the notes in search_root_dse.
+ #
+ def search_subschema_entry
+ rs = search(
+ :ignore_server_caps=>true,
+ :base=>"",
+ :scope=>SearchScope_BaseObject,
+ :attributes=>[:subschemaSubentry]
+ )
+ return Entry.new unless (rs and rs.first)
+ subschema_name = rs.first.subschemasubentry
+ return Entry.new unless (subschema_name and subschema_name.first)
+
+ rs = search(
+ :ignore_server_caps=>true,
+ :base=>subschema_name.first,
+ :scope=>SearchScope_BaseObject,
+ :filter=>"objectclass=subschema",
+ :attributes=>[:objectclasses, :attributetypes]
+ )
+
+ (rs and rs.first) or Entry.new
+ end
+
+
+ #--
+ # Convenience method to query server capabilities.
+ # Only do this once per Net::LDAP object.
+ # Note, we call a search, and we might be called from inside a search!
+ # MUST refactor the root_dse call out.
+ def paged_searches_supported?
+ @server_caps ||= search_root_dse
+ @server_caps[:supportedcontrol].include?(LdapControls::PagedResults)
+ end
+
+ end # class LDAP
+
+
+
+ class LDAP
+ # This is a private class used internally by the library. It should not be called by user code.
+ class Connection # :nodoc:
+
+ LdapVersion = 3
+ MaxSaslChallenges = 10
+
+
+ #--
+ # initialize
+ #
+ def initialize server
+ begin
+ @conn = TCPsocket.new( server[:host], server[:port] )
+ rescue
+ raise LdapError.new( "no connection to server" )
+ end
+
+ if server[:encryption]
+ setup_encryption server[:encryption]
+ end
+
+ yield self if block_given?
+ end
+
+
+ #--
+ # Helper method called only from new, and only after we have a successfully-opened
+ # @conn instance variable, which is a TCP connection.
+ # Depending on the received arguments, we establish SSL, potentially replacing
+ # the value of @conn accordingly.
+ # Don't generate any errors here if no encryption is requested.
+ # DO raise LdapError objects if encryption is requested and we have trouble setting
+ # it up. That includes if OpenSSL is not set up on the machine. (Question:
+ # how does the Ruby OpenSSL wrapper react in that case?)
+ # DO NOT filter exceptions raised by the OpenSSL library. Let them pass back
+ # to the user. That should make it easier for us to debug the problem reports.
+ # Presumably (hopefully?) that will also produce recognizable errors if someone
+ # tries to use this on a machine without OpenSSL.
+ #
+ # The simple_tls method is intended as the simplest, stupidest, easiest solution
+ # for people who want nothing more than encrypted comms with the LDAP server.
+ # It doesn't do any server-cert validation and requires nothing in the way
+ # of key files and root-cert files, etc etc.
+ # OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected
+ # TCPsocket object.
+ #
+ # The start_tls method is supported by many servers over the standard LDAP port.
+ # It does not require an alternative port for encrypted communications, as with
+ # simple_tls.
+ # Thanks for Kouhei Sutou for generously contributing the :start_tls path.
+ #
+ def setup_encryption args
+ case args[:method]
+ when :simple_tls
+ raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
+ ctx = OpenSSL::SSL::SSLContext.new
+ @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx)
+ @conn.connect
+ @conn.sync_close = true
+ # additional branches requiring server validation and peer certs, etc. go here.
+ when :start_tls
+ raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
+ msgid = next_msgid.to_ber
+ request = [StartTlsOid.to_ber].to_ber_appsequence( Net::LdapPdu::ExtendedRequest )
+ request_pkt = [msgid, request].to_ber_sequence
+ @conn.write request_pkt
+ be = @conn.read_ber(AsnSyntax)
+ raise LdapError.new("no start_tls result") if be.nil?
+ pdu = Net::LdapPdu.new(be)
+ raise LdapError.new("no start_tls result") if pdu.nil?
+ if pdu.result_code.zero?
+ ctx = OpenSSL::SSL::SSLContext.new
+ @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx)
+ @conn.connect
+ @conn.sync_close = true
+ else
+ raise LdapError.new("start_tls failed: #{pdu.result_code}")
+ end
+ else
+ raise LdapError.new( "unsupported encryption method #{args[:method]}" )
+ end
+ end
+
+ #--
+ # close
+ # This is provided as a convenience method to make
+ # sure a connection object gets closed without waiting
+ # for a GC to happen. Clients shouldn't have to call it,
+ # but perhaps it will come in handy someday.
+ def close
+ @conn.close
+ @conn = nil
+ end
+
+ #--
+ # next_msgid
+ #
+ def next_msgid
+ @msgid ||= 0
+ @msgid += 1
+ end
+
+
+ #--
+ # bind
+ #
+ def bind auth
+ meth = auth[:method]
+ if [:simple, :anonymous, :anon].include?( meth )
+ bind_simple auth
+ elsif meth == :sasl
+ bind_sasl( auth )
+ elsif meth == :gss_spnego
+ bind_gss_spnego( auth )
+ else
+ raise LdapError.new( "unsupported auth method (#{meth})" )
+ end
+ end
+
+ #--
+ # bind_simple
+ # Implements a simple user/psw authentication.