@@ -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