##// END OF EJS Templates
addded ruby-net-ldap (0.0.4) dependency in vendor/pluggin...
Jean-Philippe Lang -
r131:f50544bb156d
parent child
Show More
@@ -0,0 +1,272
1 GNU GENERAL PUBLIC LICENSE
2 Version 2, June 1991
3
4 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street,
5 Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and
6 distribute verbatim copies of this license document, but changing it is not
7 allowed.
8
9 Preamble
10
11 The licenses for most software are designed to take away your freedom to
12 share and change it. By contrast, the GNU General Public License is
13 intended to guarantee your freedom to share and change free software--to
14 make sure the software is free for all its users. This General Public
15 License applies to most of the Free Software Foundation's software and to
16 any other program whose authors commit to using it. (Some other Free
17 Software Foundation software is covered by the GNU Lesser General Public
18 License instead.) You can apply it to your programs, too.
19
20 When we speak of free software, we are referring to freedom, not price. Our
21 General Public Licenses are designed to make sure that you have the freedom
22 to distribute copies of free software (and charge for this service if you
23 wish), that you receive source code or can get it if you want it, that you
24 can change the software or use pieces of it in new free programs; and that
25 you know you can do these things.
26
27 To protect your rights, we need to make restrictions that forbid anyone to
28 deny you these rights or to ask you to surrender the rights. These
29 restrictions translate to certain responsibilities for you if you distribute
30 copies of the software, or if you modify it.
31
32 For example, if you distribute copies of such a program, whether gratis or
33 for a fee, you must give the recipients all the rights that you have. You
34 must make sure that they, too, receive or can get the source code. And you
35 must show them these terms so they know their rights.
36
37 We protect your rights with two steps: (1) copyright the software, and (2)
38 offer you this license which gives you legal permission to copy, distribute
39 and/or modify the software.
40
41 Also, for each author's protection and ours, we want to make certain that
42 everyone understands that there is no warranty for this free software. If
43 the software is modified by someone else and passed on, we want its
44 recipients to know that what they have is not the original, so that any
45 problems introduced by others will not reflect on the original authors'
46 reputations.
47
48 Finally, any free program is threatened constantly by software patents. We
49 wish to avoid the danger that redistributors of a free program will
50 individually obtain patent licenses, in effect making the program
51 proprietary. To prevent this, we have made it clear that any patent must be
52 licensed for everyone's free use or not licensed at all.
53
54 The precise terms and conditions for copying, distribution and modification
55 follow.
56
57 GNU GENERAL PUBLIC LICENSE
58 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
59
60 0. This License applies to any program or other work which contains a notice
61 placed by the copyright holder saying it may be distributed under the
62 terms of this General Public License. The "Program", below, refers to
63 any such program or work, and a "work based on the Program" means either
64 the Program or any derivative work under copyright law: that is to say, a
65 work containing the Program or a portion of it, either verbatim or with
66 modifications and/or translated into another language. (Hereinafter,
67 translation is included without limitation in the term "modification".)
68 Each licensee is addressed as "you".
69
70 Activities other than copying, distribution and modification are not
71 covered by this License; they are outside its scope. The act of running
72 the Program is not restricted, and the output from the Program is covered
73 only if its contents constitute a work based on the Program (independent
74 of having been made by running the Program). Whether that is true depends
75 on what the Program does.
76
77 1. You may copy and distribute verbatim copies of the Program's source code
78 as you receive it, in any medium, provided that you conspicuously and
79 appropriately publish on each copy an appropriate copyright notice and
80 disclaimer of warranty; keep intact all the notices that refer to this
81 License and to the absence of any warranty; and give any other recipients
82 of the Program a copy of this License along with the Program.
83
84 You may charge a fee for the physical act of transferring a copy, and you
85 may at your option offer warranty protection in exchange for a fee.
86
87 2. You may modify your copy or copies of the Program or any portion of it,
88 thus forming a work based on the Program, and copy and distribute such
89 modifications or work under the terms of Section 1 above, provided that
90 you also meet all of these conditions:
91
92 a) You must cause the modified files to carry prominent notices stating
93 that you changed the files and the date of any change.
94
95 b) You must cause any work that you distribute or publish, that in whole
96 or in part contains or is derived from the Program or any part
97 thereof, to be licensed as a whole at no charge to all third parties
98 under the terms of this License.
99
100 c) If the modified program normally reads commands interactively when
101 run, you must cause it, when started running for such interactive use
102 in the most ordinary way, to print or display an announcement
103 including an appropriate copyright notice and a notice that there is
104 no warranty (or else, saying that you provide a warranty) and that
105 users may redistribute the program under these conditions, and telling
106 the user how to view a copy of this License. (Exception: if the
107 Program itself is interactive but does not normally print such an
108 announcement, your work based on the Program is not required to print
109 an announcement.)
110
111 These requirements apply to the modified work as a whole. If
112 identifiable sections of that work are not derived from the Program, and
113 can be reasonably considered independent and separate works in
114 themselves, then this License, and its terms, do not apply to those
115 sections when you distribute them as separate works. But when you
116 distribute the same sections as part of a whole which is a work based on
117 the Program, the distribution of the whole must be on the terms of this
118 License, whose permissions for other licensees extend to the entire
119 whole, and thus to each and every part regardless of who wrote it.
120
121 Thus, it is not the intent of this section to claim rights or contest
122 your rights to work written entirely by you; rather, the intent is to
123 exercise the right to control the distribution of derivative or
124 collective works based on the Program.
125
126 In addition, mere aggregation of another work not based on the Program
127 with the Program (or with a work based on the Program) on a volume of a
128 storage or distribution medium does not bring the other work under the
129 scope of this License.
130
131 3. You may copy and distribute the Program (or a work based on it, under
132 Section 2) in object code or executable form under the terms of Sections
133 1 and 2 above provided that you also do one of the following:
134
135 a) Accompany it with the complete corresponding machine-readable source
136 code, which must be distributed under the terms of Sections 1 and 2
137 above on a medium customarily used for software interchange; or,
138
139 b) Accompany it with a written offer, valid for at least three years, to
140 give any third party, for a charge no more than your cost of
141 physically performing source distribution, a complete machine-readable
142 copy of the corresponding source code, to be distributed under the
143 terms of Sections 1 and 2 above on a medium customarily used for
144 software interchange; or,
145
146 c) Accompany it with the information you received as to the offer to
147 distribute corresponding source code. (This alternative is allowed
148 only for noncommercial distribution and only if you received the
149 program in object code or executable form with such an offer, in
150 accord with Subsection b above.)
151
152 The source code for a work means the preferred form of the work for
153 making modifications to it. For an executable work, complete source code
154 means all the source code for all modules it contains, plus any
155 associated interface definition files, plus the scripts used to control
156 compilation and installation of the executable. However, as a special
157 exception, the source code distributed need not include anything that is
158 normally distributed (in either source or binary form) with the major
159 components (compiler, kernel, and so on) of the operating system on which
160 the executable runs, unless that component itself accompanies the
161 executable.
162
163 If distribution of executable or object code is made by offering access
164 to copy from a designated place, then offering equivalent access to copy
165 the source code from the same place counts as distribution of the source
166 code, even though third parties are not compelled to copy the source
167 along with the object code.
168
169 4. You may not copy, modify, sublicense, or distribute the Program except as
170 expressly provided under this License. Any attempt otherwise to copy,
171 modify, sublicense or distribute the Program is void, and will
172 automatically terminate your rights under this License. However, parties
173 who have received copies, or rights, from you under this License will not
174 have their licenses terminated so long as such parties remain in full
175 compliance.
176
177 5. You are not required to accept this License, since you have not signed
178 it. However, nothing else grants you permission to modify or distribute
179 the Program or its derivative works. These actions are prohibited by law
180 if you do not accept this License. Therefore, by modifying or
181 distributing the Program (or any work based on the Program), you indicate
182 your acceptance of this License to do so, and all its terms and
183 conditions for copying, distributing or modifying the Program or works
184 based on it.
185
186 6. Each time you redistribute the Program (or any work based on the
187 Program), the recipient automatically receives a license from the
188 original licensor to copy, distribute or modify the Program subject to
189 these terms and conditions. You may not impose any further restrictions
190 on the recipients' exercise of the rights granted herein. You are not
191 responsible for enforcing compliance by third parties to this License.
192
193 7. If, as a consequence of a court judgment or allegation of patent
194 infringement or for any other reason (not limited to patent issues),
195 conditions are imposed on you (whether by court order, agreement or
196 otherwise) that contradict the conditions of this License, they do not
197 excuse you from the conditions of this License. If you cannot distribute
198 so as to satisfy simultaneously your obligations under this License and
199 any other pertinent obligations, then as a consequence you may not
200 distribute the Program at all. For example, if a patent license would
201 not permit royalty-free redistribution of the Program by all those who
202 receive copies directly or indirectly through you, then the only way you
203 could satisfy both it and this License would be to refrain entirely from
204 distribution of the Program.
205
206 If any portion of this section is held invalid or unenforceable under any
207 particular circumstance, the balance of the section is intended to apply
208 and the section as a whole is intended to apply in other circumstances.
209
210 It is not the purpose of this section to induce you to infringe any
211 patents or other property right claims or to contest validity of any such
212 claims; this section has the sole purpose of protecting the integrity of
213 the free software distribution system, which is implemented by public
214 license practices. Many people have made generous contributions to the
215 wide range of software distributed through that system in reliance on
216 consistent application of that system; it is up to the author/donor to
217 decide if he or she is willing to distribute software through any other
218 system and a licensee cannot impose that choice.
219
220 This section is intended to make thoroughly clear what is believed to be
221 a consequence of the rest of this License.
222
223 8. If the distribution and/or use of the Program is restricted in certain
224 countries either by patents or by copyrighted interfaces, the original
225 copyright holder who places the Program under this License may add an
226 explicit geographical distribution limitation excluding those countries,
227 so that distribution is permitted only in or among countries not thus
228 excluded. In such case, this License incorporates the limitation as if
229 written in the body of this License.
230
231 9. The Free Software Foundation may publish revised and/or new versions of
232 the General Public License from time to time. Such new versions will be
233 similar in spirit to the present version, but may differ in detail to
234 address new problems or concerns.
235
236 Each version is given a distinguishing version number. If the Program
237 specifies a version number of this License which applies to it and "any
238 later version", you have the option of following the terms and conditions
239 either of that version or of any later version published by the Free
240 Software Foundation. If the Program does not specify a version number of
241 this License, you may choose any version ever published by the Free
242 Software Foundation.
243
244 10. If you wish to incorporate parts of the Program into other free programs
245 whose distribution conditions are different, write to the author to ask
246 for permission. For software which is copyrighted by the Free Software
247 Foundation, write to the Free Software Foundation; we sometimes make
248 exceptions for this. Our decision will be guided by the two goals of
249 preserving the free status of all derivatives of our free software and
250 of promoting the sharing and reuse of software generally.
251
252 NO WARRANTY
253
254 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
255 THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
256 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
257 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
258 EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
259 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
260 ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH
261 YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
262 NECESSARY SERVICING, REPAIR OR CORRECTION.
263
264 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
265 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
266 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
267 DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
268 DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
269 (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
270 INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
271 THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR
272 OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
@@ -0,0 +1,58
1 = Net::LDAP Changelog
2
3 == Net::LDAP 0.0.4: August 15, 2006
4 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for
5 providing the rationale for this.
6 * Added a much-expanded set of special characters to the parser
7 for RFC-2254 filters. Thanks to Andre Nathan.
8 * Changed Net::LDAP#search so you can pass it a filter in string form.
9 The conversion to a Net::LDAP::Filter now happens automatically.
10 * Implemented Net::LDAP#bind_as (preliminary and subject to change).
11 Thanks for Simon Claret for valuable suggestions and for helping test.
12 * Fixed bug in Net::LDAP#open that was preventing #open from being
13 called more than one on a given Net::LDAP object.
14
15 == Net::LDAP 0.0.3: July 26, 2006
16 * Added simple TLS encryption.
17 Thanks to Garett Shulman for suggestions and for helping test.
18
19 == Net::LDAP 0.0.2: July 12, 2006
20 * Fixed malformation in distro tarball and gem.
21 * Improved documentation.
22 * Supported "paged search control."
23 * Added a range of API improvements.
24 * Thanks to Andre Nathan, andre@digirati.com.br, for valuable
25 suggestions.
26 * Added support for LE and GE search filters.
27 * Added support for Search referrals.
28 * Fixed a regression with openldap 2.2.x and higher caused
29 by the introduction of RFC-2696 controls. Thanks to Andre
30 Nathan for reporting the problem.
31 * Added support for RFC-2254 filter syntax.
32
33 == Net::LDAP 0.0.1: May 1, 2006
34 * Initial release.
35 * Client functionality is near-complete, although the APIs
36 are not guaranteed and may change depending on feedback
37 from the community.
38 * We're internally working on a Ruby-based implementation
39 of a full-featured, production-quality LDAP server,
40 which will leverage the underlying LDAP and BER functionality
41 in Net::LDAP.
42 * Please tell us if you would be interested in seeing a public
43 release of the LDAP server.
44 * Grateful acknowledgement to Austin Ziegler, who reviewed
45 this code and provided the release framework, including
46 minitar.
47
48 #--
49 # Net::LDAP for Ruby.
50 # http://rubyforge.org/projects/net-ldap/
51 # Copyright (C) 2006 by Francis Cianfrocca
52 #
53 # Available under the same terms as Ruby. See LICENCE in the main
54 # distribution for full licensing information.
55 #
56 # $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $
57 #++
58 # vim: sts=2 sw=2 ts=4 et ai tw=77
@@ -0,0 +1,55
1 Net::LDAP is copyrighted free software by Francis Cianfrocca
2 <garbagecat10@gmail.com>. You can redistribute it and/or modify it under either
3 the terms of the GPL (see the file COPYING), or the conditions below:
4
5 1. You may make and give away verbatim copies of the source form of the
6 software without restriction, provided that you duplicate all of the
7 original copyright notices and associated disclaimers.
8
9 2. You may modify your copy of the software in any way, provided that you do
10 at least ONE of the following:
11
12 a) place your modifications in the Public Domain or otherwise make them
13 Freely Available, such as by posting said modifications to Usenet or
14 an equivalent medium, or by allowing the author to include your
15 modifications in the software.
16
17 b) use the modified software only within your corporation or
18 organization.
19
20 c) rename any non-standard executables so the names do not conflict with
21 standard executables, which must also be provided.
22
23 d) make other distribution arrangements with the author.
24
25 3. You may distribute the software in object code or executable form,
26 provided that you do at least ONE of the following:
27
28 a) distribute the executables and library files of the software, together
29 with instructions (in the manual page or equivalent) on where to get
30 the original distribution.
31
32 b) accompany the distribution with the machine-readable source of the
33 software.
34
35 c) give non-standard executables non-standard names, with instructions on
36 where to get the original software distribution.
37
38 d) make other distribution arrangements with the author.
39
40 4. You may modify and include the part of the software into any other
41 software (possibly commercial). But some files in the distribution are
42 not written by the author, so that they are not under this terms.
43
44 They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some
45 files under the ./missing directory. See each file for the copying
46 condition.
47
48 5. The scripts and library files supplied as input to or produced as output
49 from the software do not automatically fall under the copyright of the
50 software, but belong to whomever generated them, and may be sold
51 commercially, and may be aggregated with this software.
52
53 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
54 WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
55 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
@@ -0,0 +1,32
1 = Net::LDAP for Ruby
2 Net::LDAP is an LDAP support library written in pure Ruby. It supports all
3 LDAP client features, and a subset of server features as well.
4
5 Homepage:: http://rubyforge.org/projects/net-ldap/
6 Copyright:: (C) 2006 by Francis Cianfrocca
7
8 Original developer: Francis Cianfrocca
9 Contributions by Austin Ziegler gratefully acknowledged.
10
11 == LICENCE NOTES
12 Please read the file LICENCE for licensing restrictions on this library. In
13 the simplest terms, this library is available under the same terms as Ruby
14 itself.
15
16 == Requirements
17 Net::LDAP requires Ruby 1.8.2 or better.
18
19 == Documentation
20 See Net::LDAP for documentation and usage samples.
21
22 #--
23 # Net::LDAP for Ruby.
24 # http://rubyforge.org/projects/net-ldap/
25 # Copyright (C) 2006 by Francis Cianfrocca
26 #
27 # Available under the same terms as Ruby. See LICENCE in the main
28 # distribution for full licensing information.
29 #
30 # $Id: README 141 2006-07-12 10:37:37Z blackhedd $
31 #++
32 # vim: sts=2 sw=2 ts=4 et ai tw=77
@@ -0,0 +1,294
1 # $Id: ber.rb 142 2006-07-26 12:20:33Z blackhedd $
2 #
3 # NET::BER
4 # Mixes ASN.1/BER convenience methods into several standard classes.
5 # Also provides BER parsing functionality.
6 #
7 #----------------------------------------------------------------------------
8 #
9 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
10 #
11 # Gmail: garbagecat10
12 #
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
26 #
27 #---------------------------------------------------------------------------
28 #
29 #
30
31
32
33
34 module Net
35
36 module BER
37
38 class BerError < Exception; end
39
40
41 # This module is for mixing into IO and IO-like objects.
42 module BERParser
43
44 # The order of these follows the class-codes in BER.
45 # Maybe this should have been a hash.
46 TagClasses = [:universal, :application, :context_specific, :private]
47
48 BuiltinSyntax = {
49 :universal => {
50 :primitive => {
51 1 => :boolean,
52 2 => :integer,
53 4 => :string,
54 10 => :integer,
55 },
56 :constructed => {
57 16 => :array,
58 17 => :array
59 }
60 }
61 }
62
63 #
64 # read_ber
65 # TODO: clean this up so it works properly with partial
66 # packets coming from streams that don't block when
67 # we ask for more data (like StringIOs). At it is,
68 # this can throw TypeErrors and other nasties.
69 #
70 def read_ber syntax=nil
71 return nil if eof?
72
73 id = getc # don't trash this value, we'll use it later
74 tag = id & 31
75 tag < 31 or raise BerError.new( "unsupported tag encoding: #{id}" )
76 tagclass = TagClasses[ id >> 6 ]
77 encoding = (id & 0x20 != 0) ? :constructed : :primitive
78
79 n = getc
80 lengthlength,contentlength = if n <= 127
81 [1,n]
82 else
83 j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc}
84 [1 + (n & 127), j]
85 end
86
87 newobj = read contentlength
88
89 objtype = nil
90 [syntax, BuiltinSyntax].each {|syn|
91 if syn && (ot = syn[tagclass]) && (ot = ot[encoding]) && ot[tag]
92 objtype = ot[tag]
93 break
94 end
95 }
96
97 obj = case objtype
98 when :boolean
99 newobj != "\000"
100 when :string
101 (newobj || "").dup
102 when :integer
103 j = 0
104 newobj.each_byte {|b| j = (j << 8) + b}
105 j
106 when :array
107 seq = []
108 sio = StringIO.new( newobj || "" )
109 # Interpret the subobject, but note how the loop
110 # is built: nil ends the loop, but false (a valid
111 # BER value) does not!
112 while (e = sio.read_ber(syntax)) != nil
113 seq << e
114 end
115 seq
116 else
117 raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" )
118 end
119
120 # Add the identifier bits into the object if it's a String or an Array.
121 # We can't add extra stuff to Fixnums and booleans, not that it makes much sense anyway.
122 obj and ([String,Array].include? obj.class) and obj.instance_eval "def ber_identifier; #{id}; end"
123 obj
124
125 end
126
127 end # module BERParser
128 end # module BER
129
130 end # module Net
131
132
133 class IO
134 include Net::BER::BERParser
135 end
136
137 require "stringio"
138 class StringIO
139 include Net::BER::BERParser
140 end
141
142 begin
143 require 'openssl'
144 class OpenSSL::SSL::SSLSocket
145 include Net::BER::BERParser
146 end
147 rescue LoadError
148 # Ignore LoadError.
149 # DON'T ignore NameError, which means the SSLSocket class
150 # is somehow unavailable on this implementation of Ruby's openssl.
151 # This may be WRONG, however, because we don't yet know how Ruby's
152 # openssl behaves on machines with no OpenSSL library. I suppose
153 # it's possible they do not fail to require 'openssl' but do not
154 # create the classes. So this code is provisional.
155 # Also, you might think that OpenSSL::SSL::SSLSocket inherits from
156 # IO so we'd pick it up above. But you'd be wrong.
157 end
158
159 class String
160 def read_ber syntax=nil
161 StringIO.new(self).read_ber(syntax)
162 end
163 end
164
165
166
167 #----------------------------------------------
168
169
170 class FalseClass
171 #
172 # to_ber
173 #
174 def to_ber
175 "\001\001\000"
176 end
177 end
178
179
180 class TrueClass
181 #
182 # to_ber
183 #
184 def to_ber
185 "\001\001\001"
186 end
187 end
188
189
190
191 class Fixnum
192 #
193 # to_ber
194 #
195 def to_ber
196 i = [self].pack('w')
197 [2, i.length].pack("CC") + i
198 end
199
200 #
201 # to_ber_enumerated
202 #
203 def to_ber_enumerated
204 i = [self].pack('w')
205 [10, i.length].pack("CC") + i
206 end
207
208 #
209 # to_ber_length_encoding
210 #
211 def to_ber_length_encoding
212 if self <= 127
213 [self].pack('C')
214 else
215 i = [self].pack('N').sub(/^[\0]+/,"")
216 [0x80 + i.length].pack('C') + i
217 end
218 end
219
220 end # class Fixnum
221
222
223 class Bignum
224
225 def to_ber
226 i = [self].pack('w')
227 i.length > 126 and raise Net::BER::BerError.new( "range error in bignum" )
228 [2, i.length].pack("CC") + i
229 end
230
231 end
232
233
234
235 class String
236 #
237 # to_ber
238 # A universal octet-string is tag number 4,
239 # but others are possible depending on the context, so we
240 # let the caller give us one.
241 # The preferred way to do this in user code is via to_ber_application_sring
242 # and to_ber_contextspecific.
243 #
244 def to_ber code = 4
245 [code].pack('C') + length.to_ber_length_encoding + self
246 end
247
248 #
249 # to_ber_application_string
250 #
251 def to_ber_application_string code
252 to_ber( 0x40 + code )
253 end
254
255 #
256 # to_ber_contextspecific
257 #
258 def to_ber_contextspecific code
259 to_ber( 0x80 + code )
260 end
261
262 end # class String
263
264
265
266 class Array
267 #
268 # to_ber_appsequence
269 # An application-specific sequence usually gets assigned
270 # a tag that is meaningful to the particular protocol being used.
271 # This is different from the universal sequence, which usually
272 # gets a tag value of 16.
273 # Now here's an interesting thing: We're adding the X.690
274 # "application constructed" code at the top of the tag byte (0x60),
275 # but some clients, notably ldapsearch, send "context-specific
276 # constructed" (0xA0). The latter would appear to violate RFC-1777,
277 # but what do I know? We may need to change this.
278 #
279
280 def to_ber id = 0; to_ber_seq_internal( 0x30 + id ); end
281 def to_ber_set id = 0; to_ber_seq_internal( 0x31 + id ); end
282 def to_ber_sequence id = 0; to_ber_seq_internal( 0x30 + id ); end
283 def to_ber_appsequence id = 0; to_ber_seq_internal( 0x60 + id ); end
284 def to_ber_contextspecific id = 0; to_ber_seq_internal( 0xA0 + id ); end
285
286 private
287 def to_ber_seq_internal code
288 s = self.to_s
289 [code].pack('C') + s.length.to_ber_length_encoding + s
290 end
291
292 end # class Array
293
294
This diff has been collapsed as it changes many lines, (1311 lines changed) Show them Hide them
@@ -0,0 +1,1311
1 # $Id: ldap.rb 154 2006-08-15 09:35:43Z blackhedd $
2 #
3 # Net::LDAP for Ruby
4 #
5 #
6 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7 #
8 # Written and maintained by Francis Cianfrocca, gmail: garbagecat10.
9 #
10 # This program is free software.
11 # You may re-distribute and/or modify this program under the same terms
12 # as Ruby itself: Ruby Distribution License or GNU General Public License.
13 #
14 #
15 # See Net::LDAP for documentation and usage samples.
16 #
17
18
19 require 'socket'
20 require 'ostruct'
21
22 begin
23 require 'openssl'
24 $net_ldap_openssl_available = true
25 rescue LoadError
26 end
27
28 require 'net/ber'
29 require 'net/ldap/pdu'
30 require 'net/ldap/filter'
31 require 'net/ldap/dataset'
32 require 'net/ldap/psw'
33 require 'net/ldap/entry'
34
35
36 module Net
37
38
39 # == Net::LDAP
40 #
41 # This library provides a pure-Ruby implementation of the
42 # LDAP client protocol, per RFC-2251.
43 # It can be used to access any server which implements the
44 # LDAP protocol.
45 #
46 # Net::LDAP is intended to provide full LDAP functionality
47 # while hiding the more arcane aspects
48 # the LDAP protocol itself, and thus presenting as Ruby-like
49 # a programming interface as possible.
50 #
51 # == Quick-start for the Impatient
52 # === Quick Example of a user-authentication against an LDAP directory:
53 #
54 # require 'rubygems'
55 # require 'net/ldap'
56 #
57 # ldap = Net::LDAP.new
58 # ldap.host = your_server_ip_address
59 # ldap.port = 389
60 # ldap.auth "joe_user", "opensesame"
61 # if ldap.bind
62 # # authentication succeeded
63 # else
64 # # authentication failed
65 # end
66 #
67 #
68 # === Quick Example of a search against an LDAP directory:
69 #
70 # require 'rubygems'
71 # require 'net/ldap'
72 #
73 # ldap = Net::LDAP.new :host => server_ip_address,
74 # :port => 389,
75 # :auth => {
76 # :method => :simple,
77 # :username => "cn=manager,dc=example,dc=com",
78 # :password => "opensesame"
79 # }
80 #
81 # filter = Net::LDAP::Filter.eq( "cn", "George*" )
82 # treebase = "dc=example,dc=com"
83 #
84 # ldap.search( :base => treebase, :filter => filter ) do |entry|
85 # puts "DN: #{entry.dn}"
86 # entry.each do |attribute, values|
87 # puts " #{attribute}:"
88 # values.each do |value|
89 # puts " --->#{value}"
90 # end
91 # end
92 # end
93 #
94 # p ldap.get_operation_result
95 #
96 #
97 # == A Brief Introduction to LDAP
98 #
99 # We're going to provide a quick, informal introduction to LDAP
100 # terminology and
101 # typical operations. If you're comfortable with this material, skip
102 # ahead to "How to use Net::LDAP." If you want a more rigorous treatment
103 # of this material, we recommend you start with the various IETF and ITU
104 # standards that relate to LDAP.
105 #
106 # === Entities
107 # LDAP is an Internet-standard protocol used to access directory servers.
108 # The basic search unit is the <i>entity,</i> which corresponds to
109 # a person or other domain-specific object.
110 # A directory service which supports the LDAP protocol typically
111 # stores information about a number of entities.
112 #
113 # === Principals
114 # LDAP servers are typically used to access information about people,
115 # but also very often about such items as printers, computers, and other
116 # resources. To reflect this, LDAP uses the term <i>entity,</i> or less
117 # commonly, <i>principal,</i> to denote its basic data-storage unit.
118 #
119 #
120 # === Distinguished Names
121 # In LDAP's view of the world,
122 # an entity is uniquely identified by a globally-unique text string
123 # called a <i>Distinguished Name,</i> originally defined in the X.400
124 # standards from which LDAP is ultimately derived.
125 # Much like a DNS hostname, a DN is a "flattened" text representation
126 # of a string of tree nodes. Also like DNS (and unlike Java package
127 # names), a DN expresses a chain of tree-nodes written from left to right
128 # in order from the most-resolved node to the most-general one.
129 #
130 # If you know the DN of a person or other entity, then you can query
131 # an LDAP-enabled directory for information (attributes) about the entity.
132 # Alternatively, you can query the directory for a list of DNs matching
133 # a set of criteria that you supply.
134 #
135 # === Attributes
136 #
137 # In the LDAP view of the world, a DN uniquely identifies an entity.
138 # Information about the entity is stored as a set of <i>Attributes.</i>
139 # An attribute is a text string which is associated with zero or more
140 # values. Most LDAP-enabled directories store a well-standardized
141 # range of attributes, and constrain their values according to standard
142 # rules.
143 #
144 # A good example of an attribute is <tt>sn,</tt> which stands for "Surname."
145 # This attribute is generally used to store a person's surname, or last name.
146 # Most directories enforce the standard convention that
147 # an entity's <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP
148 # jargon, that means that <tt>sn</tt> must be <i>present</i> and
149 # <i>single-valued.</i>
150 #
151 # Another attribute is <tt>mail,</tt> which is used to store email addresses.
152 # (No, there is no attribute called "email," perhaps because X.400 terminology
153 # predates the invention of the term <i>email.</i>) <tt>mail</tt> differs
154 # from <tt>sn</tt> in that most directories permit any number of values for the
155 # <tt>mail</tt> attribute, including zero.
156 #
157 #
158 # === Tree-Base
159 # We said above that X.400 Distinguished Names are <i>globally unique.</i>
160 # In a manner reminiscent of DNS, LDAP supposes that each directory server
161 # contains authoritative attribute data for a set of DNs corresponding
162 # to a specific sub-tree of the (notional) global directory tree.
163 # This subtree is generally configured into a directory server when it is
164 # created. It matters for this discussion because most servers will not
165 # allow you to query them unless you specify a correct tree-base.
166 #
167 # Let's say you work for the engineering department of Big Company, Inc.,
168 # whose internet domain is bigcompany.com. You may find that your departmental
169 # directory is stored in a server with a defined tree-base of
170 # ou=engineering,dc=bigcompany,dc=com
171 # You will need to supply this string as the <i>tree-base</i> when querying this
172 # directory. (Ou is a very old X.400 term meaning "organizational unit."
173 # Dc is a more recent term meaning "domain component.")
174 #
175 # === LDAP Versions
176 # (stub, discuss v2 and v3)
177 #
178 # === LDAP Operations
179 # The essential operations are: #bind, #search, #add, #modify, #delete, and #rename.
180 # ==== Bind
181 # #bind supplies a user's authentication credentials to a server, which in turn verifies
182 # or rejects them. There is a range of possibilities for credentials, but most directories
183 # support a simple username and password authentication.
184 #
185 # Taken by itself, #bind can be used to authenticate a user against information
186 # stored in a directory, for example to permit or deny access to some other resource.
187 # In terms of the other LDAP operations, most directories require a successful #bind to
188 # be performed before the other operations will be permitted. Some servers permit certain
189 # operations to be performed with an "anonymous" binding, meaning that no credentials are
190 # presented by the user. (We're glossing over a lot of platform-specific detail here.)
191 #
192 # ==== Search
193 # Calling #search against the directory involves specifying a treebase, a set of <i>search filters,</i>
194 # and a list of attribute values.
195 # The filters specify ranges of possible values for particular attributes. Multiple
196 # filters can be joined together with AND, OR, and NOT operators.
197 # A server will respond to a #search by returning a list of matching DNs together with a
198 # set of attribute values for each entity, depending on what attributes the search requested.
199 #
200 # ==== Add
201 # #add specifies a new DN and an initial set of attribute values. If the operation
202 # succeeds, a new entity with the corresponding DN and attributes is added to the directory.
203 #
204 # ==== Modify
205 # #modify specifies an entity DN, and a list of attribute operations. #modify is used to change
206 # the attribute values stored in the directory for a particular entity.
207 # #modify may add or delete attributes (which are lists of values) or it change attributes by
208 # adding to or deleting from their values.
209 # Net::LDAP provides three easier methods to modify an entry's attribute values:
210 # #add_attribute, #replace_attribute, and #delete_attribute.
211 #
212 # ==== Delete
213 # #delete specifies an entity DN. If it succeeds, the entity and all its attributes
214 # is removed from the directory.
215 #
216 # ==== Rename (or Modify RDN)
217 # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP protocol. It responds to
218 # the often-arising need to change the DN of an entity without discarding its attribute values.
219 # In earlier LDAP versions, the only way to do this was to delete the whole entity and add it
220 # again with a different DN.
221 #
222 # #rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most
223 # part of the DN string. If successful, #rename changes the entity DN so that its left-most
224 # node corresponds to the new RDN given in the request. (RDN, or "relative distinguished name,"
225 # denotes a single tree-node as expressed in a DN, which is a chain of tree nodes.)
226 #
227 # == How to use Net::LDAP
228 #
229 # To access Net::LDAP functionality in your Ruby programs, start by requiring
230 # the library:
231 #
232 # require 'net/ldap'
233 #
234 # If you installed the Gem version of Net::LDAP, and depending on your version of
235 # Ruby and rubygems, you _may_ also need to require rubygems explicitly:
236 #
237 # require 'rubygems'
238 # require 'net/ldap'
239 #
240 # Most operations with Net::LDAP start by instantiating a Net::LDAP object.
241 # The constructor for this object takes arguments specifying the network location
242 # (address and port) of the LDAP server, and also the binding (authentication)
243 # credentials, typically a username and password.
244 # Given an object of class Net:LDAP, you can then perform LDAP operations by calling
245 # instance methods on the object. These are documented with usage examples below.
246 #
247 # The Net::LDAP library is designed to be very disciplined about how it makes network
248 # connections to servers. This is different from many of the standard native-code
249 # libraries that are provided on most platforms, which share bloodlines with the
250 # original Netscape/Michigan LDAP client implementations. These libraries sought to
251 # insulate user code from the workings of the network. This is a good idea of course,
252 # but the practical effect has been confusing and many difficult bugs have been caused
253 # by the opacity of the native libraries, and their variable behavior across platforms.
254 #
255 # In general, Net::LDAP instance methods which invoke server operations make a connection
256 # to the server when the method is called. They execute the operation (typically binding first)
257 # and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection
258 # to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open
259 # closes the connection on completion of the block.
260 #
261
262 class LDAP
263
264 class LdapError < Exception; end
265
266 VERSION = "0.0.4"
267
268
269 SearchScope_BaseObject = 0
270 SearchScope_SingleLevel = 1
271 SearchScope_WholeSubtree = 2
272 SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree]
273
274 AsnSyntax = {
275 :application => {
276 :constructed => {
277 0 => :array, # BindRequest
278 1 => :array, # BindResponse
279 2 => :array, # UnbindRequest
280 3 => :array, # SearchRequest
281 4 => :array, # SearchData
282 5 => :array, # SearchResult
283 6 => :array, # ModifyRequest
284 7 => :array, # ModifyResponse
285 8 => :array, # AddRequest
286 9 => :array, # AddResponse
287 10 => :array, # DelRequest
288 11 => :array, # DelResponse
289 12 => :array, # ModifyRdnRequest
290 13 => :array, # ModifyRdnResponse
291 14 => :array, # CompareRequest
292 15 => :array, # CompareResponse
293 16 => :array, # AbandonRequest
294 19 => :array, # SearchResultReferral
295 24 => :array, # Unsolicited Notification
296 }
297 },
298 :context_specific => {
299 :primitive => {
300 0 => :string, # password
301 1 => :string, # Kerberos v4
302 2 => :string, # Kerberos v5
303 },
304 :constructed => {
305 0 => :array, # RFC-2251 Control
306 3 => :array, # Seach referral
307 }
308 }
309 }
310
311 DefaultHost = "127.0.0.1"
312 DefaultPort = 389
313 DefaultAuth = {:method => :anonymous}
314 DefaultTreebase = "dc=com"
315
316
317 ResultStrings = {
318 0 => "Success",
319 1 => "Operations Error",
320 2 => "Protocol Error",
321 3 => "Time Limit Exceeded",
322 4 => "Size Limit Exceeded",
323 12 => "Unavailable crtical extension",
324 16 => "No Such Attribute",
325 17 => "Undefined Attribute Type",
326 20 => "Attribute or Value Exists",
327 32 => "No Such Object",
328 34 => "Invalid DN Syntax",
329 48 => "Invalid DN Syntax",
330 48 => "Inappropriate Authentication",
331 49 => "Invalid Credentials",
332 50 => "Insufficient Access Rights",
333 51 => "Busy",
334 52 => "Unavailable",
335 53 => "Unwilling to perform",
336 65 => "Object Class Violation",
337 68 => "Entry Already Exists"
338 }
339
340
341 module LdapControls
342 PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
343 end
344
345
346 #
347 # LDAP::result2string
348 #
349 def LDAP::result2string code # :nodoc:
350 ResultStrings[code] || "unknown result (#{code})"
351 end
352
353
354 attr_accessor :host, :port, :base
355
356
357 # Instantiate an object of type Net::LDAP to perform directory operations.
358 # This constructor takes a Hash containing arguments, all of which are either optional or may be specified later with other methods as described below. The following arguments
359 # are supported:
360 # * :host => the LDAP server's IP-address (default 127.0.0.1)
361 # * :port => the LDAP server's TCP port (default 389)
362 # * :auth => a Hash containing authorization parameters. Currently supported values include:
363 # {:method => :anonymous} and
364 # {:method => :simple, :username => your_user_name, :password => your_password }
365 # The password parameter may be a Proc that returns a String.
366 # * :base => a default treebase parameter for searches performed against the LDAP server. If you don't give this value, then each call to #search must specify a treebase parameter. If you do give this value, then it will be used in subsequent calls to #search that do not specify a treebase. If you give a treebase value in any particular call to #search, that value will override any treebase value you give here.
367 # * :encryption => specifies the encryption to be used in communicating with the LDAP server. The value is either a Hash containing additional parameters, or the Symbol :simple_tls, which is equivalent to specifying the Hash {:method => :simple_tls}. There is a fairly large range of potential values that may be given for this parameter. See #encryption for details.
368 #
369 # Instantiating a Net::LDAP object does <i>not</i> result in network traffic to
370 # the LDAP server. It simply stores the connection and binding parameters in the
371 # object.
372 #
373 def initialize args = {}
374 @host = args[:host] || DefaultHost
375 @port = args[:port] || DefaultPort
376 @verbose = false # Make this configurable with a switch on the class.
377 @auth = args[:auth] || DefaultAuth
378 @base = args[:base] || DefaultTreebase
379 encryption args[:encryption] # may be nil
380
381 if pr = @auth[:password] and pr.respond_to?(:call)
382 @auth[:password] = pr.call
383 end
384
385 # This variable is only set when we are created with LDAP::open.
386 # All of our internal methods will connect using it, or else
387 # they will create their own.
388 @open_connection = nil
389 end
390
391 # Convenience method to specify authentication credentials to the LDAP
392 # server. Currently supports simple authentication requiring
393 # a username and password.
394 #
395 # Observe that on most LDAP servers,
396 # the username is a complete DN. However, with A/D, it's often possible
397 # to give only a user-name rather than a complete DN. In the latter
398 # case, beware that many A/D servers are configured to permit anonymous
399 # (uncredentialled) binding, and will silently accept your binding
400 # as anonymous if you give an unrecognized username. This is not usually
401 # what you want. (See #get_operation_result.)
402 #
403 # <b>Important:</b> The password argument may be a Proc that returns a string.
404 # This makes it possible for you to write client programs that solicit
405 # passwords from users or from other data sources without showing them
406 # in your code or on command lines.
407 #
408 # require 'net/ldap'
409 #
410 # ldap = Net::LDAP.new
411 # ldap.host = server_ip_address
412 # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw"
413 #
414 # Alternatively (with a password block):
415 #
416 # require 'net/ldap'
417 #
418 # ldap = Net::LDAP.new
419 # ldap.host = server_ip_address
420 # psw = proc { your_psw_function }
421 # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", psw
422 #
423 def authenticate username, password
424 password = password.call if password.respond_to?(:call)
425 @auth = {:method => :simple, :username => username, :password => password}
426 end
427
428 alias_method :auth, :authenticate
429
430 # Convenience method to specify encryption characteristics for connections
431 # to LDAP servers. Called implicitly by #new and #open, but may also be called
432 # by user code if desired.
433 # The single argument is generally a Hash (but see below for convenience alternatives).
434 # This implementation is currently a stub, supporting only a few encryption
435 # alternatives. As additional capabilities are added, more configuration values
436 # will be added here.
437 #
438 # Currently, the only supported argument is {:method => :simple_tls}.
439 # (Equivalently, you may pass the symbol :simple_tls all by itself, without
440 # enclosing it in a Hash.)
441 #
442 # The :simple_tls encryption method encrypts <i>all</i> communications with the LDAP
443 # server.
444 # It completely establishes SSL/TLS encryption with the LDAP server
445 # before any LDAP-protocol data is exchanged.
446 # There is no plaintext negotiation and no special encryption-request controls
447 # are sent to the server.
448 # <i>The :simple_tls option is the simplest, easiest way to encrypt communications
449 # between Net::LDAP and LDAP servers.</i>
450 # It's intended for cases where you have an implicit level of trust in the authenticity
451 # of the LDAP server. No validation of the LDAP server's SSL certificate is
452 # performed. This means that :simple_tls will not produce errors if the LDAP
453 # server's encryption certificate is not signed by a well-known Certification
454 # Authority.
455 # If you get communications or protocol errors when using this option, check
456 # with your LDAP server administrator. Pay particular attention to the TCP port
457 # you are connecting to. It's impossible for an LDAP server to support plaintext
458 # LDAP communications and <i>simple TLS</i> connections on the same port.
459 # The standard TCP port for unencrypted LDAP connections is 389, but the standard
460 # port for simple-TLS encrypted connections is 636. Be sure you are using the
461 # correct port.
462 #
463 # <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP control,
464 # which will enable encrypted communications on the same TCP port used for
465 # unencrypted connections.]</i>
466 #
467 def encryption args
468 if args == :simple_tls
469 args = {:method => :simple_tls}
470 end
471 @encryption = args
472 end
473
474
475 # #open takes the same parameters as #new. #open makes a network connection to the
476 # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block.
477 # Within the block, you can call any of the instance methods of Net::LDAP to
478 # perform operations against the LDAP directory. #open will perform all the
479 # operations in the user-supplied block on the same network connection, which
480 # will be closed automatically when the block finishes.
481 #
482 # # (PSEUDOCODE)
483 # auth = {:method => :simple, :username => username, :password => password}
484 # Net::LDAP.open( :host => ipaddress, :port => 389, :auth => auth ) do |ldap|
485 # ldap.search( ... )
486 # ldap.add( ... )
487 # ldap.modify( ... )
488 # end
489 #
490 def LDAP::open args
491 ldap1 = LDAP.new args
492 ldap1.open {|ldap| yield ldap }
493 end
494
495 # Returns a meaningful result any time after
496 # a protocol operation (#bind, #search, #add, #modify, #rename, #delete)
497 # has completed.
498 # It returns an #OpenStruct containing an LDAP result code (0 means success),
499 # and a human-readable string.
500 # unless ldap.bind
501 # puts "Result: #{ldap.get_operation_result.code}"
502 # puts "Message: #{ldap.get_operation_result.message}"
503 # end
504 #
505 def get_operation_result
506 os = OpenStruct.new
507 if @result
508 os.code = @result
509 else
510 os.code = 0
511 end
512 os.message = LDAP.result2string( os.code )
513 os
514 end
515
516
517 # Opens a network connection to the server and then
518 # passes <tt>self</tt> to the caller-supplied block. The connection is
519 # closed when the block completes. Used for executing multiple
520 # LDAP operations without requiring a separate network connection
521 # (and authentication) for each one.
522 # <i>Note:</i> You do not need to log-in or "bind" to the server. This will
523 # be done for you automatically.
524 # For an even simpler approach, see the class method Net::LDAP#open.
525 #
526 # # (PSEUDOCODE)
527 # auth = {:method => :simple, :username => username, :password => password}
528 # ldap = Net::LDAP.new( :host => ipaddress, :port => 389, :auth => auth )
529 # ldap.open do |ldap|
530 # ldap.search( ... )
531 # ldap.add( ... )
532 # ldap.modify( ... )
533 # end
534 #--
535 # First we make a connection and then a binding, but we don't
536 # do anything with the bind results.
537 # We then pass self to the caller's block, where he will execute
538 # his LDAP operations. Of course they will all generate auth failures
539 # if the bind was unsuccessful.
540 def open
541 raise LdapError.new( "open already in progress" ) if @open_connection
542 @open_connection = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
543 @open_connection.bind @auth
544 yield self
545 @open_connection.close
546 @open_connection = nil
547 end
548
549
550 # Searches the LDAP directory for directory entries.
551 # Takes a hash argument with parameters. Supported parameters include:
552 # * :base (a string specifying the tree-base for the search);
553 # * :filter (an object of type Net::LDAP::Filter, defaults to objectclass=*);
554 # * :attributes (a string or array of strings specifying the LDAP attributes to return from the server);
555 # * :return_result (a boolean specifying whether to return a result set).
556 # * :attributes_only (a boolean flag, defaults false)
557 # * :scope (one of: Net::LDAP::SearchScope_BaseObject, Net::LDAP::SearchScope_SingleLevel, Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
558 #
559 # #search queries the LDAP server and passes <i>each entry</i> to the
560 # caller-supplied block, as an object of type Net::LDAP::Entry.
561 # If the search returns 1000 entries, the block will
562 # be called 1000 times. If the search returns no entries, the block will
563 # not be called.
564 #
565 #--
566 # ORIGINAL TEXT, replaced 04May06.
567 # #search returns either a result-set or a boolean, depending on the
568 # value of the <tt>:return_result</tt> argument. The default behavior is to return
569 # a result set, which is a hash. Each key in the hash is a string specifying
570 # the DN of an entry. The corresponding value for each key is a Net::LDAP::Entry object.
571 # If you request a result set and #search fails with an error, it will return nil.
572 # Call #get_operation_result to get the error information returned by
573 # the LDAP server.
574 #++
575 # #search returns either a result-set or a boolean, depending on the
576 # value of the <tt>:return_result</tt> argument. The default behavior is to return
577 # a result set, which is an Array of objects of class Net::LDAP::Entry.
578 # If you request a result set and #search fails with an error, it will return nil.
579 # Call #get_operation_result to get the error information returned by
580 # the LDAP server.
581 #
582 # When <tt>:return_result => false,</tt> #search will
583 # return only a Boolean, to indicate whether the operation succeeded. This can improve performance
584 # with very large result sets, because the library can discard each entry from memory after
585 # your block processes it.
586 #
587 #
588 # treebase = "dc=example,dc=com"
589 # filter = Net::LDAP::Filter.eq( "mail", "a*.com" )
590 # attrs = ["mail", "cn", "sn", "objectclass"]
591 # ldap.search( :base => treebase, :filter => filter, :attributes => attrs, :return_result => false ) do |entry|
592 # puts "DN: #{entry.dn}"
593 # entry.each do |attr, values|
594 # puts ".......#{attr}:"
595 # values.each do |value|
596 # puts " #{value}"
597 # end
598 # end
599 # end
600 #
601 #--
602 # This is a re-implementation of search that replaces the
603 # original one (now renamed searchx and possibly destined to go away).
604 # The difference is that we return a dataset (or nil) from the
605 # call, and pass _each entry_ as it is received from the server
606 # to the caller-supplied block. This will probably make things
607 # far faster as we can do useful work during the network latency
608 # of the search. The downside is that we have no access to the
609 # whole set while processing the blocks, so we can't do stuff
610 # like sort the DNs until after the call completes.
611 # It's also possible that this interacts badly with server timeouts.
612 # We'll have to ensure that something reasonable happens if
613 # the caller has processed half a result set when we throw a timeout
614 # error.
615 # Another important difference is that we return a result set from
616 # this method rather than a T/F indication.
617 # Since this can be very heavy-weight, we define an argument flag
618 # that the caller can set to suppress the return of a result set,
619 # if he's planning to process every entry as it comes from the server.
620 #
621 # REINTERPRETED the result set, 04May06. Originally this was a hash
622 # of entries keyed by DNs. But let's get away from making users
623 # handle DNs. Change it to a plain array. Eventually we may
624 # want to return a Dataset object that delegates to an internal
625 # array, so we can provide sort methods and what-not.
626 #
627 def search args = {}
628 args[:base] ||= @base
629 result_set = (args and args[:return_result] == false) ? nil : []
630
631 if @open_connection
632 @result = @open_connection.search( args ) {|entry|
633 result_set << entry if result_set
634 yield( entry ) if block_given?
635 }
636 else
637 @result = 0
638 conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
639 if (@result = conn.bind( args[:auth] || @auth )) == 0
640 @result = conn.search( args ) {|entry|
641 result_set << entry if result_set
642 yield( entry ) if block_given?
643 }
644 end
645 conn.close
646 end
647
648 @result == 0 and result_set
649 end
650
651 # #bind connects to an LDAP server and requests authentication
652 # based on the <tt>:auth</tt> parameter passed to #open or #new.
653 # It takes no parameters.
654 #
655 # User code does not need to call #bind directly. It will be called
656 # implicitly by the library whenever you invoke an LDAP operation,
657 # such as #search or #add.
658 #
659 # It is useful, however, to call #bind in your own code when the
660 # only operation you intend to perform against the directory is
661 # to validate a login credential. #bind returns true or false
662 # to indicate whether the binding was successful. Reasons for
663 # failure include malformed or unrecognized usernames and
664 # incorrect passwords. Use #get_operation_result to find out
665 # what happened in case of failure.
666 #
667 # Here's a typical example using #bind to authenticate a
668 # credential which was (perhaps) solicited from the user of a
669 # web site:
670 #
671 # require 'net/ldap'
672 # ldap = Net::LDAP.new
673 # ldap.host = your_server_ip_address
674 # ldap.port = 389
675 # ldap.auth your_user_name, your_user_password
676 # if ldap.bind
677 # # authentication succeeded
678 # else
679 # # authentication failed
680 # p ldap.get_operation_result
681 # end
682 #
683 # You don't have to create a new instance of Net::LDAP every time
684 # you perform a binding in this way. If you prefer, you can cache the Net::LDAP object
685 # and re-use it to perform subsequent bindings, <i>provided</i> you call
686 # #auth to specify a new credential before calling #bind. Otherwise, you'll
687 # just re-authenticate the previous user! (You don't need to re-set
688 # the values of #host and #port.) As noted in the documentation for #auth,
689 # the password parameter can be a Ruby Proc instead of a String.
690 #
691 #--
692 # If there is an @open_connection, then perform the bind
693 # on it. Otherwise, connect, bind, and disconnect.
694 # The latter operation is obviously useful only as an auth check.
695 #
696 def bind auth=@auth
697 if @open_connection
698 @result = @open_connection.bind auth
699 else
700 conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption)
701 @result = conn.bind @auth
702 conn.close
703 end
704
705 @result == 0
706 end
707
708 #
709 # #bind_as is for testing authentication credentials.
710 #
711 # As described under #bind, most LDAP servers require that you supply a complete DN
712 # as a binding-credential, along with an authenticator such as a password.
713 # But for many applications (such as authenticating users to a Rails application),
714 # you often don't have a full DN to identify the user. You usually get a simple
715 # identifier like a username or an email address, along with a password.
716 # #bind_as allows you to authenticate these user-identifiers.
717 #
718 # #bind_as is a combination of a search and an LDAP binding. First, it connects and
719 # binds to the directory as normal. Then it searches the directory for an entry
720 # corresponding to the email address, username, or other string that you supply.
721 # If the entry exists, then #bind_as will <b>re-bind</b> as that user with the
722 # password (or other authenticator) that you supply.
723 #
724 # #bind_as takes the same parameters as #search, <i>with the addition of an
725 # authenticator.</i> Currently, this authenticator must be <tt>:password</tt>.
726 # Its value may be either a String, or a +proc+ that returns a String.
727 # #bind_as returns +false+ on failure. On success, it returns a result set,
728 # just as #search does. This result set is an Array of objects of
729 # type Net::LDAP::Entry. It contains the directory attributes corresponding to
730 # the user. (Just test whether the return value is logically true, if you don't
731 # need this additional information.)
732 #
733 # Here's how you would use #bind_as to authenticate an email address and password:
734 #
735 # require 'net/ldap'
736 #
737 # user,psw = "joe_user@yourcompany.com", "joes_psw"
738 #
739 # ldap = Net::LDAP.new
740 # ldap.host = "192.168.0.100"
741 # ldap.port = 389
742 # ldap.auth "cn=manager,dc=yourcompany,dc=com", "topsecret"
743 #
744 # result = ldap.bind_as(
745 # :base => "dc=yourcompany,dc=com",
746 # :filter => "(mail=#{user})",
747 # :password => psw
748 # )
749 # if result
750 # puts "Authenticated #{result.first.dn}"
751 # else
752 # puts "Authentication FAILED."
753 # end
754 def bind_as args={}
755 result = false
756 open {|me|
757 rs = search args
758 if rs and rs.first and dn = rs.first.dn
759 password = args[:password]
760 password = password.call if password.respond_to?(:call)
761 result = rs if bind :method => :simple, :username => dn, :password => password
762 end
763 }
764 result
765 end
766
767
768 # Adds a new entry to the remote LDAP server.
769 # Supported arguments:
770 # :dn :: Full DN of the new entry
771 # :attributes :: Attributes of the new entry.
772 #
773 # The attributes argument is supplied as a Hash keyed by Strings or Symbols
774 # giving the attribute name, and mapping to Strings or Arrays of Strings
775 # giving the actual attribute values. Observe that most LDAP directories
776 # enforce schema constraints on the attributes contained in entries.
777 # #add will fail with a server-generated error if your attributes violate
778 # the server-specific constraints.
779 # Here's an example:
780 #
781 # dn = "cn=George Smith,ou=people,dc=example,dc=com"
782 # attr = {
783 # :cn => "George Smith",
784 # :objectclass => ["top", "inetorgperson"],
785 # :sn => "Smith",
786 # :mail => "gsmith@example.com"
787 # }
788 # Net::LDAP.open (:host => host) do |ldap|
789 # ldap.add( :dn => dn, :attributes => attr )
790 # end
791 #
792 def add args
793 if @open_connection
794 @result = @open_connection.add( args )
795 else
796 @result = 0
797 conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption)
798 if (@result = conn.bind( args[:auth] || @auth )) == 0
799 @result = conn.add( args )
800 end
801 conn.close
802 end
803 @result == 0
804 end
805
806
807 # Modifies the attribute values of a particular entry on the LDAP directory.
808 # Takes a hash with arguments. Supported arguments are:
809 # :dn :: (the full DN of the entry whose attributes are to be modified)
810 # :operations :: (the modifications to be performed, detailed next)
811 #
812 # This method returns True or False to indicate whether the operation
813 # succeeded or failed, with extended information available by calling
814 # #get_operation_result.
815 #
816 # Also see #add_attribute, #replace_attribute, or #delete_attribute, which
817 # provide simpler interfaces to this functionality.
818 #
819 # The LDAP protocol provides a full and well thought-out set of operations
820 # for changing the values of attributes, but they are necessarily somewhat complex
821 # and not always intuitive. If these instructions are confusing or incomplete,
822 # please send us email or create a bug report on rubyforge.
823 #
824 # The :operations parameter to #modify takes an array of operation-descriptors.
825 # Each individual operation is specified in one element of the array, and
826 # most LDAP servers will attempt to perform the operations in order.
827 #
828 # Each of the operations appearing in the Array must itself be an Array
829 # with exactly three elements:
830 # an operator:: must be :add, :replace, or :delete
831 # an attribute name:: the attribute name (string or symbol) to modify
832 # a value:: either a string or an array of strings.
833 #
834 # The :add operator will, unsurprisingly, add the specified values to
835 # the specified attribute. If the attribute does not already exist,
836 # :add will create it. Most LDAP servers will generate an error if you
837 # try to add a value that already exists.
838 #
839 # :replace will erase the current value(s) for the specified attribute,
840 # if there are any, and replace them with the specified value(s).
841 #
842 # :delete will remove the specified value(s) from the specified attribute.
843 # If you pass nil, an empty string, or an empty array as the value parameter
844 # to a :delete operation, the _entire_ _attribute_ will be deleted, along
845 # with all of its values.
846 #
847 # For example:
848 #
849 # dn = "mail=modifyme@example.com,ou=people,dc=example,dc=com"
850 # ops = [
851 # [:add, :mail, "aliasaddress@example.com"],
852 # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]],
853 # [:delete, :sn, nil]
854 # ]
855 # ldap.modify :dn => dn, :operations => ops
856 #
857 # <i>(This example is contrived since you probably wouldn't add a mail
858 # value right before replacing the whole attribute, but it shows that order
859 # of execution matters. Also, many LDAP servers won't let you delete SN
860 # because that would be a schema violation.)</i>
861 #
862 # It's essential to keep in mind that if you specify more than one operation in
863 # a call to #modify, most LDAP servers will attempt to perform all of the operations
864 # in the order you gave them.
865 # This matters because you may specify operations on the
866 # same attribute which must be performed in a certain order.
867 #
868 # Most LDAP servers will _stop_ processing your modifications if one of them
869 # causes an error on the server (such as a schema-constraint violation).
870 # If this happens, you will probably get a result code from the server that
871 # reflects only the operation that failed, and you may or may not get extended
872 # information that will tell you which one failed. #modify has no notion
873 # of an atomic transaction. If you specify a chain of modifications in one
874 # call to #modify, and one of them fails, the preceding ones will usually
875 # not be "rolled back," resulting in a partial update. This is a limitation
876 # of the LDAP protocol, not of Net::LDAP.
877 #
878 # The lack of transactional atomicity in LDAP means that you're usually
879 # better off using the convenience methods #add_attribute, #replace_attribute,
880 # and #delete_attribute, which are are wrappers over #modify. However, certain
881 # LDAP servers may provide concurrency semantics, in which the several operations
882 # contained in a single #modify call are not interleaved with other
883 # modification-requests received simultaneously by the server.
884 # It bears repeating that this concurrency does _not_ imply transactional
885 # atomicity, which LDAP does not provide.
886 #
887 def modify args
888 if @open_connection
889 @result = @open_connection.modify( args )
890 else
891 @result = 0
892 conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
893 if (@result = conn.bind( args[:auth] || @auth )) == 0
894 @result = conn.modify( args )
895 end
896 conn.close
897 end
898 @result == 0
899 end
900
901
902 # Add a value to an attribute.
903 # Takes the full DN of the entry to modify,
904 # the name (Symbol or String) of the attribute, and the value (String or
905 # Array). If the attribute does not exist (and there are no schema violations),
906 # #add_attribute will create it with the caller-specified values.
907 # If the attribute already exists (and there are no schema violations), the
908 # caller-specified values will be _added_ to the values already present.
909 #
910 # Returns True or False to indicate whether the operation
911 # succeeded or failed, with extended information available by calling
912 # #get_operation_result. See also #replace_attribute and #delete_attribute.
913 #
914 # dn = "cn=modifyme,dc=example,dc=com"
915 # ldap.add_attribute dn, :mail, "newmailaddress@example.com"
916 #
917 def add_attribute dn, attribute, value
918 modify :dn => dn, :operations => [[:add, attribute, value]]
919 end
920
921 # Replace the value of an attribute.
922 # #replace_attribute can be thought of as equivalent to calling #delete_attribute
923 # followed by #add_attribute. It takes the full DN of the entry to modify,
924 # the name (Symbol or String) of the attribute, and the value (String or
925 # Array). If the attribute does not exist, it will be created with the
926 # caller-specified value(s). If the attribute does exist, its values will be
927 # _discarded_ and replaced with the caller-specified values.
928 #
929 # Returns True or False to indicate whether the operation
930 # succeeded or failed, with extended information available by calling
931 # #get_operation_result. See also #add_attribute and #delete_attribute.
932 #
933 # dn = "cn=modifyme,dc=example,dc=com"
934 # ldap.replace_attribute dn, :mail, "newmailaddress@example.com"
935 #
936 def replace_attribute dn, attribute, value
937 modify :dn => dn, :operations => [[:replace, attribute, value]]
938 end
939
940 # Delete an attribute and all its values.
941 # Takes the full DN of the entry to modify, and the
942 # name (Symbol or String) of the attribute to delete.
943 #
944 # Returns True or False to indicate whether the operation
945 # succeeded or failed, with extended information available by calling
946 # #get_operation_result. See also #add_attribute and #replace_attribute.
947 #
948 # dn = "cn=modifyme,dc=example,dc=com"
949 # ldap.delete_attribute dn, :mail
950 #
951 def delete_attribute dn, attribute
952 modify :dn => dn, :operations => [[:delete, attribute, nil]]
953 end
954
955
956 # Rename an entry on the remote DIS by changing the last RDN of its DN.
957 # _Documentation_ _stub_
958 #
959 def rename args
960 if @open_connection
961 @result = @open_connection.rename( args )
962 else
963 @result = 0
964 conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
965 if (@result = conn.bind( args[:auth] || @auth )) == 0
966 @result = conn.rename( args )
967 end
968 conn.close
969 end
970 @result == 0
971 end
972
973 # modify_rdn is an alias for #rename.
974 def modify_rdn args
975 rename args
976 end
977
978 # Delete an entry from the LDAP directory.
979 # Takes a hash of arguments.
980 # The only supported argument is :dn, which must
981 # give the complete DN of the entry to be deleted.
982 # Returns True or False to indicate whether the delete
983 # succeeded. Extended status information is available by
984 # calling #get_operation_result.
985 #
986 # dn = "mail=deleteme@example.com,ou=people,dc=example,dc=com"
987 # ldap.delete :dn => dn
988 #
989 def delete args
990 if @open_connection
991 @result = @open_connection.delete( args )
992 else
993 @result = 0
994 conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
995 if (@result = conn.bind( args[:auth] || @auth )) == 0
996 @result = conn.delete( args )
997 end
998 conn.close
999 end
1000 @result == 0
1001 end
1002
1003 end # class LDAP
1004
1005
1006
1007 class LDAP
1008 # This is a private class used internally by the library. It should not be called by user code.
1009 class Connection # :nodoc:
1010
1011 LdapVersion = 3
1012
1013
1014 #--
1015 # initialize
1016 #
1017 def initialize server
1018 begin
1019 @conn = TCPsocket.new( server[:host], server[:port] )
1020 rescue
1021 raise LdapError.new( "no connection to server" )
1022 end
1023
1024 if server[:encryption]
1025 setup_encryption server[:encryption]
1026 end
1027
1028 yield self if block_given?
1029 end
1030
1031
1032 #--
1033 # Helper method called only from new, and only after we have a successfully-opened
1034 # @conn instance variable, which is a TCP connection.
1035 # Depending on the received arguments, we establish SSL, potentially replacing
1036 # the value of @conn accordingly.
1037 # Don't generate any errors here if no encryption is requested.
1038 # DO raise LdapError objects if encryption is requested and we have trouble setting
1039 # it up. That includes if OpenSSL is not set up on the machine. (Question:
1040 # how does the Ruby OpenSSL wrapper react in that case?)
1041 # DO NOT filter exceptions raised by the OpenSSL library. Let them pass back
1042 # to the user. That should make it easier for us to debug the problem reports.
1043 # Presumably (hopefully?) that will also produce recognizable errors if someone
1044 # tries to use this on a machine without OpenSSL.
1045 #
1046 # The simple_tls method is intended as the simplest, stupidest, easiest solution
1047 # for people who want nothing more than encrypted comms with the LDAP server.
1048 # It doesn't do any server-cert validation and requires nothing in the way
1049 # of key files and root-cert files, etc etc.
1050 # OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected
1051 # TCPsocket object.
1052 #
1053 def setup_encryption args
1054 case args[:method]
1055 when :simple_tls
1056 raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
1057 ctx = OpenSSL::SSL::SSLContext.new
1058 @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx)
1059 @conn.connect
1060 @conn.sync_close = true
1061 # additional branches requiring server validation and peer certs, etc. go here.
1062 else
1063 raise LdapError.new( "unsupported encryption method #{args[:method]}" )
1064 end
1065 end
1066
1067 #--
1068 # close
1069 # This is provided as a convenience method to make
1070 # sure a connection object gets closed without waiting
1071 # for a GC to happen. Clients shouldn't have to call it,
1072 # but perhaps it will come in handy someday.
1073 def close
1074 @conn.close
1075 @conn = nil
1076 end
1077
1078 #--
1079 # next_msgid
1080 #
1081 def next_msgid
1082 @msgid ||= 0
1083 @msgid += 1
1084 end
1085
1086
1087 #--
1088 # bind
1089 #
1090 def bind auth
1091 user,psw = case auth[:method]
1092 when :anonymous
1093 ["",""]
1094 when :simple
1095 [auth[:username] || auth[:dn], auth[:password]]
1096 end
1097 raise LdapError.new( "invalid binding information" ) unless (user && psw)
1098
1099 msgid = next_msgid.to_ber
1100 request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
1101 request_pkt = [msgid, request].to_ber_sequence
1102 @conn.write request_pkt
1103
1104 (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
1105 pdu.result_code
1106 end
1107
1108 #--
1109 # search
1110 # Alternate implementation, this yields each search entry to the caller
1111 # as it are received.
1112 # TODO, certain search parameters are hardcoded.
1113 # TODO, if we mis-parse the server results or the results are wrong, we can block
1114 # forever. That's because we keep reading results until we get a type-5 packet,
1115 # which might never come. We need to support the time-limit in the protocol.
1116 #--
1117 # WARNING: this code substantially recapitulates the searchx method.
1118 #
1119 # 02May06: Well, I added support for RFC-2696-style paged searches.
1120 # This is used on all queries because the extension is marked non-critical.
1121 # As far as I know, only A/D uses this, but it's required for A/D. Otherwise
1122 # you won't get more than 1000 results back from a query.
1123 # This implementation is kindof clunky and should probably be refactored.
1124 # Also, is it my imagination, or are A/Ds the slowest directory servers ever???
1125 #
1126 def search args = {}
1127 search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
1128 search_filter = Filter.construct(search_filter) if search_filter.is_a?(String)
1129 search_base = (args && args[:base]) || "dc=example,dc=com"
1130 search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
1131 return_referrals = args && args[:return_referrals] == true
1132
1133 attributes_only = (args and args[:attributes_only] == true)
1134 scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
1135 raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
1136
1137 # An interesting value for the size limit would be close to A/D's built-in
1138 # page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes
1139 # on anything bigger than 126. You get a silent error that is easily visible
1140 # by running slapd in debug mode. Go figure.
1141 rfc2696_cookie = [126, ""]
1142 result_code = 0
1143
1144 loop {
1145 # should collect this into a private helper to clarify the structure
1146
1147 request = [
1148 search_base.to_ber,
1149 scope.to_ber_enumerated,
1150 0.to_ber_enumerated,
1151 0.to_ber,
1152 0.to_ber,
1153 attributes_only.to_ber,
1154 search_filter.to_ber,
1155 search_attributes.to_ber_sequence
1156 ].to_ber_appsequence(3)
1157
1158 controls = [
1159 [
1160 LdapControls::PagedResults.to_ber,
1161 false.to_ber, # criticality MUST be false to interoperate with normal LDAPs.
1162 rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber
1163 ].to_ber_sequence
1164 ].to_ber_contextspecific(0)
1165
1166 pkt = [next_msgid.to_ber, request, controls].to_ber_sequence
1167 @conn.write pkt
1168
1169 result_code = 0
1170 controls = []
1171
1172 while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
1173 case pdu.app_tag
1174 when 4 # search-data
1175 yield( pdu.search_entry ) if block_given?
1176 when 19 # search-referral
1177 if return_referrals
1178 if block_given?
1179 se = Net::LDAP::Entry.new
1180 se[:search_referrals] = (pdu.search_referrals || [])
1181 yield se
1182 end
1183 end
1184 #p pdu.referrals
1185 when 5 # search-result
1186 result_code = pdu.result_code
1187 controls = pdu.result_controls
1188 break
1189 else
1190 raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
1191 end
1192 end
1193
1194 # When we get here, we have seen a type-5 response.
1195 # If there is no error AND there is an RFC-2696 cookie,
1196 # then query again for the next page of results.
1197 # If not, we're done.
1198 # Don't screw this up or we'll break every search we do.
1199 more_pages = false
1200 if result_code == 0 and controls
1201 controls.each do |c|
1202 if c.oid == LdapControls::PagedResults
1203 more_pages = false # just in case some bogus server sends us >1 of these.
1204 if c.value and c.value.length > 0
1205 cookie = c.value.read_ber[1]
1206 if cookie and cookie.length > 0
1207 rfc2696_cookie[1] = cookie
1208 more_pages = true
1209 end
1210 end
1211 end
1212 end
1213 end
1214
1215 break unless more_pages
1216 } # loop
1217
1218 result_code
1219 end
1220
1221
1222
1223
1224 #--
1225 # modify
1226 # TODO, need to support a time limit, in case the server fails to respond.
1227 # TODO!!! We're throwing an exception here on empty DN.
1228 # Should return a proper error instead, probaby from farther up the chain.
1229 # TODO!!! If the user specifies a bogus opcode, we'll throw a
1230 # confusing error here ("to_ber_enumerated is not defined on nil").
1231 #
1232 def modify args
1233 modify_dn = args[:dn] or raise "Unable to modify empty DN"
1234 modify_ops = []
1235 a = args[:operations] and a.each {|op, attr, values|
1236 # TODO, fix the following line, which gives a bogus error
1237 # if the opcode is invalid.
1238 op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
1239 modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence
1240 }
1241
1242 request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
1243 pkt = [next_msgid.to_ber, request].to_ber_sequence
1244 @conn.write pkt
1245
1246 (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
1247 pdu.result_code
1248 end
1249
1250
1251 #--
1252 # add
1253 # TODO, need to support a time limit, in case the server fails to respond.
1254 #
1255 def add args
1256 add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
1257 add_attrs = []
1258 a = args[:attributes] and a.each {|k,v|
1259 add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
1260 }
1261
1262 request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
1263 pkt = [next_msgid.to_ber, request].to_ber_sequence
1264 @conn.write pkt
1265
1266 (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" )
1267 pdu.result_code
1268 end
1269
1270
1271 #--
1272 # rename
1273 # TODO, need to support a time limit, in case the server fails to respond.
1274 #
1275 def rename args
1276 old_dn = args[:olddn] or raise "Unable to rename empty DN"
1277 new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
1278 delete_attrs = args[:delete_attributes] ? true : false
1279
1280 request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
1281 pkt = [next_msgid.to_ber, request].to_ber_sequence
1282 @conn.write pkt
1283
1284 (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
1285 pdu.result_code
1286 end
1287
1288
1289 #--
1290 # delete
1291 # TODO, need to support a time limit, in case the server fails to respond.
1292 #
1293 def delete args
1294 dn = args[:dn] or raise "Unable to delete empty DN"
1295
1296 request = dn.to_s.to_ber_application_string(10)
1297 pkt = [next_msgid.to_ber, request].to_ber_sequence
1298 @conn.write pkt
1299
1300 (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" )
1301 pdu.result_code
1302 end
1303
1304
1305 end # class Connection
1306 end # class LDAP
1307
1308
1309 end # module Net
1310
1311
@@ -0,0 +1,108
1 # $Id: dataset.rb 78 2006-04-26 02:57:34Z blackhedd $
2 #
3 #
4 #----------------------------------------------------------------------------
5 #
6 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7 #
8 # Gmail: garbagecat10
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 #
24 #---------------------------------------------------------------------------
25 #
26 #
27
28
29
30
31 module Net
32 class LDAP
33
34 class Dataset < Hash
35
36 attr_reader :comments
37
38
39 def Dataset::read_ldif io
40 ds = Dataset.new
41
42 line = io.gets && chomp
43 dn = nil
44
45 while line
46 io.gets and chomp
47 if $_ =~ /^[\s]+/
48 line << " " << $'
49 else
50 nextline = $_
51
52 if line =~ /^\#/
53 ds.comments << line
54 elsif line =~ /^dn:[\s]*/i
55 dn = $'
56 ds[dn] = Hash.new {|k,v| k[v] = []}
57 elsif line.length == 0
58 dn = nil
59 elsif line =~ /^([^:]+):([\:]?)[\s]*/
60 # $1 is the attribute name
61 # $2 is a colon iff the attr-value is base-64 encoded
62 # $' is the attr-value
63 # Avoid the Base64 class because not all Ruby versions have it.
64 attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
65 ds[dn][$1.downcase.intern] << attrvalue
66 end
67
68 line = nextline
69 end
70 end
71
72 ds
73 end
74
75
76 def initialize
77 @comments = []
78 end
79
80
81 def to_ldif
82 ary = []
83 ary += (@comments || [])
84
85 keys.sort.each {|dn|
86 ary << "dn: #{dn}"
87
88 self[dn].keys.map {|sym| sym.to_s}.sort.each {|attr|
89 self[dn][attr.intern].each {|val|
90 ary << "#{attr}: #{val}"
91 }
92 }
93
94 ary << ""
95 }
96
97 block_given? and ary.each {|line| yield line}
98
99 ary
100 end
101
102
103 end # Dataset
104
105 end # LDAP
106 end # Net
107
108
@@ -0,0 +1,165
1 # $Id: entry.rb 123 2006-05-18 03:52:38Z blackhedd $
2 #
3 # LDAP Entry (search-result) support classes
4 #
5 #
6 #----------------------------------------------------------------------------
7 #
8 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
9 #
10 # Gmail: garbagecat10
11 #
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
25 #
26 #---------------------------------------------------------------------------
27 #
28
29
30
31
32 module Net
33 class LDAP
34
35
36 # Objects of this class represent individual entries in an LDAP
37 # directory. User code generally does not instantiate this class.
38 # Net::LDAP#search provides objects of this class to user code,
39 # either as block parameters or as return values.
40 #
41 # In LDAP-land, an "entry" is a collection of attributes that are
42 # uniquely and globally identified by a DN ("Distinguished Name").
43 # Attributes are identified by short, descriptive words or phrases.
44 # Although a directory is
45 # free to implement any attribute name, most of them follow rigorous
46 # standards so that the range of commonly-encountered attribute
47 # names is not large.
48 #
49 # An attribute name is case-insensitive. Most directories also
50 # restrict the range of characters allowed in attribute names.
51 # To simplify handling attribute names, Net::LDAP::Entry
52 # internally converts them to a standard format. Therefore, the
53 # methods which take attribute names can take Strings or Symbols,
54 # and work correctly regardless of case or capitalization.
55 #
56 # An attribute consists of zero or more data items called
57 # <i>values.</i> An entry is the combination of a unique DN, a set of attribute
58 # names, and a (possibly-empty) array of values for each attribute.
59 #
60 # Class Net::LDAP::Entry provides convenience methods for dealing
61 # with LDAP entries.
62 # In addition to the methods documented below, you may access individual
63 # attributes of an entry simply by giving the attribute name as
64 # the name of a method call. For example:
65 # ldap.search( ... ) do |entry|
66 # puts "Common name: #{entry.cn}"
67 # puts "Email addresses:"
68 # entry.mail.each {|ma| puts ma}
69 # end
70 # If you use this technique to access an attribute that is not present
71 # in a particular Entry object, a NoMethodError exception will be raised.
72 #
73 #--
74 # Ugly problem to fix someday: We key off the internal hash with
75 # a canonical form of the attribute name: convert to a string,
76 # downcase, then take the symbol. Unfortunately we do this in
77 # at least three places. Should do it in ONE place.
78 class Entry
79
80 # This constructor is not generally called by user code.
81 def initialize dn = nil # :nodoc:
82 @myhash = Hash.new {|k,v| k[v] = [] }
83 @myhash[:dn] = [dn]
84 end
85
86
87 def []= name, value # :nodoc:
88 sym = name.to_s.downcase.intern
89 @myhash[sym] = value
90 end
91
92
93 #--
94 # We have to deal with this one as we do with []=
95 # because this one and not the other one gets called
96 # in formulations like entry["CN"] << cn.
97 #
98 def [] name # :nodoc:
99 name = name.to_s.downcase.intern unless name.is_a?(Symbol)
100 @myhash[name]
101 end
102
103 # Returns the dn of the Entry as a String.
104 def dn
105 self[:dn][0]
106 end
107
108 # Returns an array of the attribute names present in the Entry.
109 def attribute_names
110 @myhash.keys
111 end
112
113 # Accesses each of the attributes present in the Entry.
114 # Calls a user-supplied block with each attribute in turn,
115 # passing two arguments to the block: a Symbol giving
116 # the name of the attribute, and a (possibly empty)
117 # Array of data values.
118 #
119 def each
120 if block_given?
121 attribute_names.each {|a|
122 attr_name,values = a,self[a]
123 yield attr_name, values
124 }
125 end
126 end
127
128 alias_method :each_attribute, :each
129
130
131 #--
132 # Convenience method to convert unknown method names
133 # to attribute references. Of course the method name
134 # comes to us as a symbol, so let's save a little time
135 # and not bother with the to_s.downcase two-step.
136 # Of course that means that a method name like mAIL
137 # won't work, but we shouldn't be encouraging that
138 # kind of bad behavior in the first place.
139 # Maybe we should thow something if the caller sends
140 # arguments or a block...
141 #
142 def method_missing *args, &block # :nodoc:
143 s = args[0].to_s.downcase.intern
144 if attribute_names.include?(s)
145 self[s]
146 elsif s.to_s[-1] == 61 and s.to_s.length > 1
147 value = args[1] or raise RuntimeError.new( "unable to set value" )
148 value = [value] unless value.is_a?(Array)
149 name = s.to_s[0..-2].intern
150 self[name] = value
151 else
152 raise NoMethodError.new( "undefined method '#{s}'" )
153 end
154 end
155
156 def write
157 end
158
159 end # class Entry
160
161
162 end # class LDAP
163 end # module Net
164
165
@@ -0,0 +1,387
1 # $Id: filter.rb 151 2006-08-15 08:34:53Z blackhedd $
2 #
3 #
4 #----------------------------------------------------------------------------
5 #
6 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7 #
8 # Gmail: garbagecat10
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 #
24 #---------------------------------------------------------------------------
25 #
26 #
27
28
29 module Net
30 class LDAP
31
32
33 # Class Net::LDAP::Filter is used to constrain
34 # LDAP searches. An object of this class is
35 # passed to Net::LDAP#search in the parameter :filter.
36 #
37 # Net::LDAP::Filter supports the complete set of search filters
38 # available in LDAP, including conjunction, disjunction and negation
39 # (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
40 # standard notation for specifying LDAP search filters.
41 #
42 # Here's how to code the familiar "objectclass is present" filter:
43 # f = Net::LDAP::Filter.pres( "objectclass" )
44 # The object returned by this code can be passed directly to
45 # the <tt>:filter</tt> parameter of Net::LDAP#search.
46 #
47 # See the individual class and instance methods below for more examples.
48 #
49 class Filter
50
51 def initialize op, a, b
52 @op = op
53 @left = a
54 @right = b
55 end
56
57 # #eq creates a filter object indicating that the value of
58 # a paticular attribute must be either <i>present</i> or must
59 # match a particular string.
60 #
61 # To specify that an attribute is "present" means that only
62 # directory entries which contain a value for the particular
63 # attribute will be selected by the filter. This is useful
64 # in case of optional attributes such as <tt>mail.</tt>
65 # Presence is indicated by giving the value "*" in the second
66 # parameter to #eq. This example selects only entries that have
67 # one or more values for <tt>sAMAccountName:</tt>
68 # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
69 #
70 # To match a particular range of values, pass a string as the
71 # second parameter to #eq. The string may contain one or more
72 # "*" characters as wildcards: these match zero or more occurrences
73 # of any character. Full regular-expressions are <i>not</i> supported
74 # due to limitations in the underlying LDAP protocol.
75 # This example selects any entry with a <tt>mail</tt> value containing
76 # the substring "anderson":
77 # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
78 #--
79 # Removed gt and lt. They ain't in the standard!
80 #
81 def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
82 def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
83 #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
84 #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
85 def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
86 def Filter::le attribute, value; Filter.new :le, attribute, value; end
87
88 # #pres( attribute ) is a synonym for #eq( attribute, "*" )
89 #
90 def Filter::pres attribute; Filter.eq attribute, "*"; end
91
92 # operator & ("AND") is used to conjoin two or more filters.
93 # This expression will select only entries that have an <tt>objectclass</tt>
94 # attribute AND have a <tt>mail</tt> attribute that begins with "George":
95 # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
96 #
97 def & filter; Filter.new :and, self, filter; end
98
99 # operator | ("OR") is used to disjoin two or more filters.
100 # This expression will select entries that have either an <tt>objectclass</tt>
101 # attribute OR a <tt>mail</tt> attribute that begins with "George":
102 # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
103 #
104 def | filter; Filter.new :or, self, filter; end
105
106
107 #
108 # operator ~ ("NOT") is used to negate a filter.
109 # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
110 # attribute:
111 # f = ~ Net::LDAP::Filter.pres( "objectclass" )
112 #
113 #--
114 # This operator can't be !, evidently. Try it.
115 # Removed GT and LT. They're not in the RFC.
116 def ~@; Filter.new :not, self, nil; end
117
118
119 def to_s
120 case @op
121 when :ne
122 "(!(#{@left}=#{@right}))"
123 when :eq
124 "(#{@left}=#{@right})"
125 #when :gt
126 # "#{@left}>#{@right}"
127 #when :lt
128 # "#{@left}<#{@right}"
129 when :ge
130 "#{@left}>=#{@right}"
131 when :le
132 "#{@left}<=#{@right}"
133 when :and
134 "(&(#{@left})(#{@right}))"
135 when :or
136 "(|(#{@left})(#{@right}))"
137 when :not
138 "(!(#{@left}))"
139 else
140 raise "invalid or unsupported operator in LDAP Filter"
141 end
142 end
143
144
145 #--
146 # to_ber
147 # Filter ::=
148 # CHOICE {
149 # and [0] SET OF Filter,
150 # or [1] SET OF Filter,
151 # not [2] Filter,
152 # equalityMatch [3] AttributeValueAssertion,
153 # substrings [4] SubstringFilter,
154 # greaterOrEqual [5] AttributeValueAssertion,
155 # lessOrEqual [6] AttributeValueAssertion,
156 # present [7] AttributeType,
157 # approxMatch [8] AttributeValueAssertion
158 # }
159 #
160 # SubstringFilter
161 # SEQUENCE {
162 # type AttributeType,
163 # SEQUENCE OF CHOICE {
164 # initial [0] LDAPString,
165 # any [1] LDAPString,
166 # final [2] LDAPString
167 # }
168 # }
169 #
170 # Parsing substrings is a little tricky.
171 # We use the split method to break a string into substrings
172 # delimited by the * (star) character. But we also need
173 # to know whether there is a star at the head and tail
174 # of the string. A Ruby particularity comes into play here:
175 # if you split on * and the first character of the string is
176 # a star, then split will return an array whose first element
177 # is an _empty_ string. But if the _last_ character of the
178 # string is star, then split will return an array that does
179 # _not_ add an empty string at the end. So we have to deal
180 # with all that specifically.
181 #
182 def to_ber
183 case @op
184 when :eq
185 if @right == "*" # present
186 @left.to_s.to_ber_contextspecific 7
187 elsif @right =~ /[\*]/ #substring
188 ary = @right.split( /[\*]+/ )
189 final_star = @right =~ /[\*]$/
190 initial_star = ary.first == "" and ary.shift
191
192 seq = []
193 unless initial_star
194 seq << ary.shift.to_ber_contextspecific(0)
195 end
196 n_any_strings = ary.length - (final_star ? 0 : 1)
197 #p n_any_strings
198 n_any_strings.times {
199 seq << ary.shift.to_ber_contextspecific(1)
200 }
201 unless final_star
202 seq << ary.shift.to_ber_contextspecific(2)
203 end
204 [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
205 else #equality
206 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
207 end
208 when :ge
209 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
210 when :le
211 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
212 when :and
213 ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
214 ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
215 when :or
216 ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
217 ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
218 when :not
219 [@left.to_ber].to_ber_contextspecific 2
220 else
221 # ERROR, we'll return objectclass=* to keep things from blowing up,
222 # but that ain't a good answer and we need to kick out an error of some kind.
223 raise "unimplemented search filter"
224 end
225 end
226
227 #--
228 # coalesce
229 # This is a private helper method for dealing with chains of ANDs and ORs
230 # that are longer than two. If BOTH of our branches are of the specified
231 # type of joining operator, then return both of them as an array (calling
232 # coalesce recursively). If they're not, then return an array consisting
233 # only of self.
234 #
235 def coalesce operator
236 if @op == operator
237 [@left.coalesce( operator ), @right.coalesce( operator )]
238 else
239 [self]
240 end
241 end
242
243
244
245 #--
246 # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
247 # object. Convert it to a Net::LDAP::Filter.
248 # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
249 # filter types. Could pull them out into a constant.
250 #
251 def Filter::parse_ldap_filter obj
252 case obj.ber_identifier
253 when 0x87 # present. context-specific primitive 7.
254 Filter.eq( obj.to_s, "*" )
255 when 0xa3 # equalityMatch. context-specific constructed 3.
256 Filter.eq( obj[0], obj[1] )
257 else
258 raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
259 end
260 end
261
262
263 #--
264 # We got a hash of attribute values.
265 # Do we match the attributes?
266 # Return T/F, and call match recursively as necessary.
267 def match entry
268 case @op
269 when :eq
270 if @right == "*"
271 l = entry[@left] and l.length > 0
272 else
273 l = entry[@left] and l = l.to_a and l.index(@right)
274 end
275 else
276 raise LdapError.new( "unknown filter type in match: #{@op}" )
277 end
278 end
279
280 # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
281 # to a Net::LDAP::Filter.
282 def self.construct ldap_filter_string
283 FilterParser.new(ldap_filter_string).filter
284 end
285
286 # Synonym for #construct.
287 # to a Net::LDAP::Filter.
288 def self.from_rfc2254 ldap_filter_string
289 construct ldap_filter_string
290 end
291
292 end # class Net::LDAP::Filter
293
294
295
296 class FilterParser #:nodoc:
297
298 attr_reader :filter
299
300 def initialize str
301 require 'strscan'
302 @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
303 end
304
305 def parse scanner
306 parse_filter_branch(scanner) or parse_paren_expression(scanner)
307 end
308
309 def parse_paren_expression scanner
310 if scanner.scan /\s*\(\s*/
311 b = if scanner.scan /\s*\&\s*/
312 a = nil
313 branches = []
314 while br = parse_paren_expression(scanner)
315 branches << br
316 end
317 if branches.length >= 2
318 a = branches.shift
319 while branches.length > 0
320 a = a & branches.shift
321 end
322 a
323 end
324 elsif scanner.scan /\s*\|\s*/
325 # TODO: DRY!
326 a = nil
327 branches = []
328 while br = parse_paren_expression(scanner)
329 branches << br
330 end
331 if branches.length >= 2
332 a = branches.shift
333 while branches.length > 0
334 a = a | branches.shift
335 end
336 a
337 end
338 elsif scanner.scan /\s*\!\s*/
339 br = parse_paren_expression(scanner)
340 if br
341 ~ br
342 end
343 else
344 parse_filter_branch( scanner )
345 end
346
347 if b and scanner.scan( /\s*\)\s*/ )
348 b
349 end
350 end
351 end
352
353 # Added a greatly-augmented filter contributed by Andre Nathan
354 # for detecting special characters in values. (15Aug06)
355 def parse_filter_branch scanner
356 scanner.scan /\s*/
357 if token = scanner.scan( /[\w\-_]+/ )
358 scanner.scan /\s*/
359 if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
360 scanner.scan /\s*/
361 #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
362 if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ )
363 case op
364 when "="
365 Filter.eq( token, value )
366 when "!="
367 Filter.ne( token, value )
368 when "<"
369 Filter.lt( token, value )
370 when "<="
371 Filter.le( token, value )
372 when ">"
373 Filter.gt( token, value )
374 when ">="
375 Filter.ge( token, value )
376 end
377 end
378 end
379 end
380 end
381
382 end # class Net::LDAP::FilterParser
383
384 end # class Net::LDAP
385 end # module Net
386
387
@@ -0,0 +1,205
1 # $Id: pdu.rb 126 2006-05-31 15:55:16Z blackhedd $
2 #
3 # LDAP PDU support classes
4 #
5 #
6 #----------------------------------------------------------------------------
7 #
8 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
9 #
10 # Gmail: garbagecat10
11 #
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
25 #
26 #---------------------------------------------------------------------------
27 #
28
29
30
31 module Net
32
33
34 class LdapPduError < Exception; end
35
36
37 class LdapPdu
38
39 BindResult = 1
40 SearchReturnedData = 4
41 SearchResult = 5
42 ModifyResponse = 7
43 AddResponse = 9
44 DeleteResponse = 11
45 ModifyRDNResponse = 13
46 SearchResultReferral = 19
47
48 attr_reader :msg_id, :app_tag
49 attr_reader :search_dn, :search_attributes, :search_entry
50 attr_reader :search_referrals
51
52 #
53 # initialize
54 # An LDAP PDU always looks like a BerSequence with
55 # at least two elements: an integer (message-id number), and
56 # an application-specific sequence.
57 # Some LDAPv3 packets also include an optional
58 # third element, which is a sequence of "controls"
59 # (See RFC 2251, section 4.1.12).
60 # The application-specific tag in the sequence tells
61 # us what kind of packet it is, and each kind has its
62 # own format, defined in RFC-1777.
63 # Observe that many clients (such as ldapsearch)
64 # do not necessarily enforce the expected application
65 # tags on received protocol packets. This implementation
66 # does interpret the RFC strictly in this regard, and
67 # it remains to be seen whether there are servers out
68 # there that will not work well with our approach.
69 #
70 # Added a controls-processor to SearchResult.
71 # Didn't add it everywhere because it just _feels_
72 # like it will need to be refactored.
73 #
74 def initialize ber_object
75 begin
76 @msg_id = ber_object[0].to_i
77 @app_tag = ber_object[1].ber_identifier - 0x60
78 rescue
79 # any error becomes a data-format error
80 raise LdapPduError.new( "ldap-pdu format error" )
81 end
82
83 case @app_tag
84 when BindResult
85 parse_ldap_result ber_object[1]
86 when SearchReturnedData
87 parse_search_return ber_object[1]
88 when SearchResultReferral
89 parse_search_referral ber_object[1]
90 when SearchResult
91 parse_ldap_result ber_object[1]
92 parse_controls(ber_object[2]) if ber_object[2]
93 when ModifyResponse
94 parse_ldap_result ber_object[1]
95 when AddResponse
96 parse_ldap_result ber_object[1]
97 when DeleteResponse
98 parse_ldap_result ber_object[1]
99 when ModifyRDNResponse
100 parse_ldap_result ber_object[1]
101 else
102 raise LdapPduError.new( "unknown pdu-type: #{@app_tag}" )
103 end
104 end
105
106 #
107 # result_code
108 # This returns an LDAP result code taken from the PDU,
109 # but it will be nil if there wasn't a result code.
110 # That can easily happen depending on the type of packet.
111 #
112 def result_code code = :resultCode
113 @ldap_result and @ldap_result[code]
114 end
115
116 # Return RFC-2251 Controls if any.
117 # Messy. Does this functionality belong somewhere else?
118 def result_controls
119 @ldap_controls || []
120 end
121
122
123 #
124 # parse_ldap_result
125 #
126 def parse_ldap_result sequence
127 sequence.length >= 3 or raise LdapPduError
128 @ldap_result = {:resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2]}
129 end
130 private :parse_ldap_result
131
132 #
133 # parse_search_return
134 # Definition from RFC 1777 (we're handling application-4 here)
135 #
136 # Search Response ::=
137 # CHOICE {
138 # entry [APPLICATION 4] SEQUENCE {
139 # objectName LDAPDN,
140 # attributes SEQUENCE OF SEQUENCE {
141 # AttributeType,
142 # SET OF AttributeValue
143 # }
144 # },
145 # resultCode [APPLICATION 5] LDAPResult
146 # }
147 #
148 # We concoct a search response that is a hash of the returned attribute values.
149 # NOW OBSERVE CAREFULLY: WE ARE DOWNCASING THE RETURNED ATTRIBUTE NAMES.
150 # This is to make them more predictable for user programs, but it
151 # may not be a good idea. Maybe this should be configurable.
152 # ALTERNATE IMPLEMENTATION: In addition to @search_dn and @search_attributes,
153 # we also return @search_entry, which is an LDAP::Entry object.
154 # If that works out well, then we'll remove the first two.
155 #
156 # Provisionally removed obsolete search_attributes and search_dn, 04May06.
157 #
158 def parse_search_return sequence
159 sequence.length >= 2 or raise LdapPduError
160 @search_entry = LDAP::Entry.new( sequence[0] )
161 #@search_dn = sequence[0]
162 #@search_attributes = {}
163 sequence[1].each {|seq|
164 @search_entry[seq[0]] = seq[1]
165 #@search_attributes[seq[0].downcase.intern] = seq[1]
166 }
167 end
168
169 #
170 # A search referral is a sequence of one or more LDAP URIs.
171 # Any number of search-referral replies can be returned by the server, interspersed
172 # with normal replies in any order.
173 # Until I can think of a better way to do this, we'll return the referrals as an array.
174 # It'll be up to higher-level handlers to expose something reasonable to the client.
175 def parse_search_referral uris
176 @search_referrals = uris
177 end
178
179
180 # Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting
181 # of an OID, a boolean criticality flag defaulting FALSE, and an OPTIONAL
182 # Octet String. If only two fields are given, the second one may be
183 # either criticality or data, since criticality has a default value.
184 # Someday we may want to come back here and add support for some of
185 # more-widely used controls. RFC-2696 is a good example.
186 #
187 def parse_controls sequence
188 @ldap_controls = sequence.map do |control|
189 o = OpenStruct.new
190 o.oid,o.criticality,o.value = control[0],control[1],control[2]
191 if o.criticality and o.criticality.is_a?(String)
192 o.value = o.criticality
193 o.criticality = false
194 end
195 o
196 end
197 end
198 private :parse_controls
199
200
201 end
202
203
204 end # module Net
205
@@ -0,0 +1,64
1 # $Id: psw.rb 73 2006-04-24 21:59:35Z blackhedd $
2 #
3 #
4 #----------------------------------------------------------------------------
5 #
6 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7 #
8 # Gmail: garbagecat10
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 #
24 #---------------------------------------------------------------------------
25 #
26 #
27
28
29 module Net
30 class LDAP
31
32
33 class Password
34 class << self
35
36 # Generate a password-hash suitable for inclusion in an LDAP attribute.
37 # Pass a hash type (currently supported: :md5 and :sha) and a plaintext
38 # password. This function will return a hashed representation.
39 # STUB: This is here to fulfill the requirements of an RFC, which one?
40 # TODO, gotta do salted-sha and (maybe) salted-md5.
41 # Should we provide sha1 as a synonym for sha1? I vote no because then
42 # should you also provide ssha1 for symmetry?
43 def generate( type, str )
44 case type
45 when :md5
46 require 'md5'
47 "{MD5}#{ [MD5.new( str.to_s ).digest].pack("m").chomp }"
48 when :sha
49 require 'sha1'
50 "{SHA}#{ [SHA1.new( str.to_s ).digest].pack("m").chomp }"
51 # when ssha
52 else
53 raise Net::LDAP::LdapError.new( "unsupported password-hash type (#{type})" )
54 end
55 end
56
57 end
58 end
59
60
61 end # class LDAP
62 end # module Net
63
64
@@ -0,0 +1,39
1 # $Id: ldif.rb 78 2006-04-26 02:57:34Z blackhedd $
2 #
3 # Net::LDIF for Ruby
4 #
5 #
6 #
7 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
8 #
9 # Gmail: garbagecat10
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24 #
25 #
26
27 # THIS FILE IS A STUB.
28
29 module Net
30
31 class LDIF
32
33
34 end # class LDIF
35
36
37 end # module Net
38
39
@@ -0,0 +1,42
1 # $Id: testber.rb 57 2006-04-18 00:18:48Z blackhedd $
2 #
3 #
4
5
6 $:.unshift "lib"
7
8 require 'net/ldap'
9 require 'stringio'
10
11
12 class TestBer < Test::Unit::TestCase
13
14 def setup
15 end
16
17 # TODO: Add some much bigger numbers
18 # 5000000000 is a Bignum, which hits different code.
19 def test_ber_integers
20 assert_equal( "\002\001\005", 5.to_ber )
21 assert_equal( "\002\002\203t", 500.to_ber )
22 assert_equal( "\002\003\203\206P", 50000.to_ber )
23 assert_equal( "\002\005\222\320\227\344\000", 5000000000.to_ber )
24 end
25
26 def test_ber_parsing
27 assert_equal( 6, "\002\001\006".read_ber( Net::LDAP::AsnSyntax ))
28 assert_equal( "testing", "\004\007testing".read_ber( Net::LDAP::AsnSyntax ))
29 end
30
31
32 def test_ber_parser_on_ldap_bind_request
33 s = StringIO.new "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus"
34 assert_equal( [1, [3, "Administrator", "ad_is_bogus"]], s.read_ber( Net::LDAP::AsnSyntax ))
35 end
36
37
38
39
40 end
41
42
@@ -0,0 +1,101
1 # $Id: testdata.ldif 50 2006-04-17 17:57:33Z blackhedd $
2 #
3 # This is test-data for an LDAP server in LDIF format.
4 #
5 dn: dc=bayshorenetworks,dc=com
6 objectClass: dcObject
7 objectClass: organization
8 o: Bayshore Networks LLC
9 dc: bayshorenetworks
10
11 dn: cn=Manager,dc=bayshorenetworks,dc=com
12 objectClass: organizationalrole
13 cn: Manager
14
15 dn: ou=people,dc=bayshorenetworks,dc=com
16 objectClass: organizationalunit
17 ou: people
18
19 dn: ou=privileges,dc=bayshorenetworks,dc=com
20 objectClass: organizationalunit
21 ou: privileges
22
23 dn: ou=roles,dc=bayshorenetworks,dc=com
24 objectClass: organizationalunit
25 ou: roles
26
27 dn: ou=office,dc=bayshorenetworks,dc=com
28 objectClass: organizationalunit
29 ou: office
30
31 dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com
32 cn: Bob Fosse
33 mail: nogoodnik@steamheat.net
34 sn: Fosse
35 ou: people
36 objectClass: top
37 objectClass: inetorgperson
38 objectClass: authorizedperson
39 hasAccessRole: uniqueIdentifier=engineer,ou=roles
40 hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles
41 hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles
42 hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles
43 hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles
44 hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles
45 hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles
46 hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles
47 hasAccessRole: uniqueIdentifier=workorder_user,ou=roles
48 hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles
49 hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles
50 hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles
51
52 dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com
53 cn: Gwen Verdon
54 mail: elephant@steamheat.net
55 sn: Verdon
56 ou: people
57 objectClass: top
58 objectClass: inetorgperson
59 objectClass: authorizedperson
60 hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles
61 hasAccessRole: uniqueIdentifier=engineer,ou=roles
62 hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles
63 hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles
64 hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles
65
66 dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com
67 uniqueIdentifier: engineering
68 ou: privileges
69 objectClass: accessPrivilege
70
71 dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com
72 uniqueIdentifier: engineer
73 ou: roles
74 objectClass: accessRole
75 hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges
76
77 dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com
78 uniqueIdentifier: ldapadmin
79 ou: roles
80 objectClass: accessRole
81
82 dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com
83 uniqueIdentifier: ldapsuperadmin
84 ou: roles
85 objectClass: accessRole
86
87 dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com
88 cn: Sid Sorokin
89 mail: catperson@steamheat.net
90 sn: Sorokin
91 ou: people
92 objectClass: top
93 objectClass: inetorgperson
94 objectClass: authorizedperson
95 hasAccessRole: uniqueIdentifier=engineer,ou=roles
96 hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles
97 hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles
98 hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles
99 hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles
100 hasAccessRole: uniqueIdentifier=workorder_user,ou=roles
101
@@ -0,0 +1,12
1 # $Id: testem.rb 121 2006-05-15 18:36:24Z blackhedd $
2 #
3 #
4
5 require 'test/unit'
6 require 'tests/testber'
7 require 'tests/testldif'
8 require 'tests/testldap'
9 require 'tests/testpsw'
10 require 'tests/testfilter'
11
12
@@ -0,0 +1,37
1 # $Id: testfilter.rb 122 2006-05-15 20:03:56Z blackhedd $
2 #
3 #
4
5 require 'test/unit'
6
7 $:.unshift "lib"
8
9 require 'net/ldap'
10
11
12 class TestFilter < Test::Unit::TestCase
13
14 def setup
15 end
16
17
18 def teardown
19 end
20
21 def test_rfc_2254
22 p Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " )
23 p Net::LDAP::Filter.from_rfc2254( "uid!=george*" )
24 p Net::LDAP::Filter.from_rfc2254( "uid<george*" )
25 p Net::LDAP::Filter.from_rfc2254( "uid <= george*" )
26 p Net::LDAP::Filter.from_rfc2254( "uid>george*" )
27 p Net::LDAP::Filter.from_rfc2254( "uid>=george*" )
28 p Net::LDAP::Filter.from_rfc2254( "uid!=george*" )
29
30 p Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" )
31 p Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" )
32 p Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" )
33 end
34
35
36 end
37
@@ -0,0 +1,190
1 # $Id: testldap.rb 65 2006-04-23 01:17:49Z blackhedd $
2 #
3 #
4
5
6 $:.unshift "lib"
7
8 require 'test/unit'
9
10 require 'net/ldap'
11 require 'stringio'
12
13
14 class TestLdapClient < Test::Unit::TestCase
15
16 # TODO: these tests crash and burn if the associated
17 # LDAP testserver isn't up and running.
18 # We rely on being able to read a file with test data
19 # in LDIF format.
20 # TODO, WARNING: for the moment, this data is in a file
21 # whose name and location are HARDCODED into the
22 # instance method load_test_data.
23
24 def setup
25 @host = "127.0.0.1"
26 @port = 3890
27 @auth = {
28 :method => :simple,
29 :username => "cn=bigshot,dc=bayshorenetworks,dc=com",
30 :password => "opensesame"
31 }
32
33 @ldif = load_test_data
34 end
35
36
37
38 # Get some test data which will be used to validate
39 # the responses from the test LDAP server we will
40 # connect to.
41 # TODO, Bogus: we are HARDCODING the location of the file for now.
42 #
43 def load_test_data
44 ary = File.readlines( "tests/testdata.ldif" )
45 hash = {}
46 while line = ary.shift and line.chomp!
47 if line =~ /^dn:[\s]*/i
48 dn = $'
49 hash[dn] = {}
50 while attr = ary.shift and attr.chomp! and attr =~ /^([\w]+)[\s]*:[\s]*/
51 hash[dn][$1.downcase.intern] ||= []
52 hash[dn][$1.downcase.intern] << $'
53 end
54 end
55 end
56 hash
57 end
58
59
60
61 # Binding tests.
62 # Need tests for all kinds of network failures and incorrect auth.
63 # TODO: Implement a class-level timeout for operations like bind.
64 # Search has a timeout defined at the protocol level, other ops do not.
65 # TODO, use constants for the LDAP result codes, rather than hardcoding them.
66 def test_bind
67 ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
68 assert_equal( true, ldap.bind )
69 assert_equal( 0, ldap.get_operation_result.code )
70 assert_equal( "Success", ldap.get_operation_result.message )
71
72 bad_username = @auth.merge( {:username => "cn=badguy,dc=imposters,dc=com"} )
73 ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_username
74 assert_equal( false, ldap.bind )
75 assert_equal( 48, ldap.get_operation_result.code )
76 assert_equal( "Inappropriate Authentication", ldap.get_operation_result.message )
77
78 bad_password = @auth.merge( {:password => "cornhusk"} )
79 ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_password
80 assert_equal( false, ldap.bind )
81 assert_equal( 49, ldap.get_operation_result.code )
82 assert_equal( "Invalid Credentials", ldap.get_operation_result.message )
83 end
84
85
86
87 def test_search
88 ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
89
90 search = {:base => "dc=smalldomain,dc=com"}
91 assert_equal( false, ldap.search( search ))
92 assert_equal( 32, ldap.get_operation_result.code )
93
94 search = {:base => "dc=bayshorenetworks,dc=com"}
95 assert_equal( true, ldap.search( search ))
96 assert_equal( 0, ldap.get_operation_result.code )
97
98 ldap.search( search ) {|res|
99 assert_equal( res, @ldif )
100 }
101 end
102
103
104
105
106 # This is a helper routine for test_search_attributes.
107 def internal_test_search_attributes attrs_to_search
108 ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
109 assert( ldap.bind )
110
111 search = {
112 :base => "dc=bayshorenetworks,dc=com",
113 :attributes => attrs_to_search
114 }
115
116 ldif = @ldif
117 ldif.each {|dn,entry|
118 entry.delete_if {|attr,value|
119 ! attrs_to_search.include?(attr)
120 }
121 }
122
123 assert_equal( true, ldap.search( search ))
124 ldap.search( search ) {|res|
125 res_keys = res.keys.sort
126 ldif_keys = ldif.keys.sort
127 assert( res_keys, ldif_keys )
128 res.keys.each {|rk|
129 assert( res[rk], ldif[rk] )
130 }
131 }
132 end
133
134
135 def test_search_attributes
136 internal_test_search_attributes [:mail]
137 internal_test_search_attributes [:cn]
138 internal_test_search_attributes [:ou]
139 internal_test_search_attributes [:hasaccessprivilege]
140 internal_test_search_attributes ["mail"]
141 internal_test_search_attributes ["cn"]
142 internal_test_search_attributes ["ou"]
143 internal_test_search_attributes ["hasaccessrole"]
144
145 internal_test_search_attributes [:mail, :cn, :ou, :hasaccessrole]
146 internal_test_search_attributes [:mail, "cn", :ou, "hasaccessrole"]
147 end
148
149
150 def test_search_filters
151 ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
152 search = {
153 :base => "dc=bayshorenetworks,dc=com",
154 :filter => Net::LDAP::Filter.eq( "sn", "Fosse" )
155 }
156
157 ldap.search( search ) {|res|
158 p res
159 }
160 end
161
162
163
164 def test_open
165 ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
166 ldap.open {|ldap|
167 10.times {
168 rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" )
169 assert_equal( true, rc )
170 }
171 }
172 end
173
174
175 def test_ldap_open
176 Net::LDAP.open( :host => @host, :port => @port, :auth => @auth ) {|ldap|
177 10.times {
178 rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" )
179 assert_equal( true, rc )
180 }
181 }
182 end
183
184
185
186
187
188 end
189
190
@@ -0,0 +1,69
1 # $Id: testldif.rb 61 2006-04-18 20:55:55Z blackhedd $
2 #
3 #
4
5
6 $:.unshift "lib"
7
8 require 'test/unit'
9
10 require 'net/ldap'
11 require 'net/ldif'
12
13 require 'sha1'
14 require 'base64'
15
16 class TestLdif < Test::Unit::TestCase
17
18 TestLdifFilename = "tests/testdata.ldif"
19
20 def test_empty_ldif
21 ds = Net::LDAP::Dataset::read_ldif( StringIO.new )
22 assert_equal( true, ds.empty? )
23 end
24
25 def test_ldif_with_comments
26 str = ["# Hello from LDIF-land", "# This is an unterminated comment"]
27 io = StringIO.new( str[0] + "\r\n" + str[1] )
28 ds = Net::LDAP::Dataset::read_ldif( io )
29 assert_equal( str, ds.comments )
30 end
31
32 def test_ldif_with_password
33 psw = "goldbricks"
34 hashed_psw = "{SHA}" + Base64::encode64( SHA1.new(psw).digest ).chomp
35
36 ldif_encoded = Base64::encode64( hashed_psw ).chomp
37 ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n" ))
38 recovered_psw = ds["Goldbrick"][:userpassword].shift
39 assert_equal( hashed_psw, recovered_psw )
40 end
41
42 def test_ldif_with_continuation_lines
43 ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: abcdefg\r\n hijklmn\r\n\r\n" ))
44 assert_equal( true, ds.has_key?( "abcdefg hijklmn" ))
45 end
46
47 # TODO, INADEQUATE. We need some more tests
48 # to verify the content.
49 def test_ldif
50 File.open( TestLdifFilename, "r" ) {|f|
51 ds = Net::LDAP::Dataset::read_ldif( f )
52 assert_equal( 13, ds.length )
53 }
54 end
55
56 # TODO, need some tests.
57 # Must test folded lines and base64-encoded lines as well as normal ones.
58 def test_to_ldif
59 File.open( TestLdifFilename, "r" ) {|f|
60 ds = Net::LDAP::Dataset::read_ldif( f )
61 ds.to_ldif
62 assert_equal( true, false ) # REMOVE WHEN WE HAVE SOME TESTS HERE.
63 }
64 end
65
66
67 end
68
69
@@ -0,0 +1,28
1 # $Id: testpsw.rb 72 2006-04-24 21:58:14Z blackhedd $
2 #
3 #
4
5
6 $:.unshift "lib"
7
8 require 'net/ldap'
9 require 'stringio'
10
11
12 class TestPassword < Test::Unit::TestCase
13
14 def setup
15 end
16
17
18 def test_psw
19 assert_equal( "{MD5}xq8jwrcfibi0sZdZYNkSng==", Net::LDAP::Password.generate( :md5, "cashflow" ))
20 assert_equal( "{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", Net::LDAP::Password.generate( :sha, "cashflow" ))
21 end
22
23
24
25
26 end
27
28
General Comments 0
You need to be logged in to leave comments. Login now