passiveldap 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +272 -0
- data/ChangeLog +22 -0
- data/LICENCE +55 -0
- data/README +28 -0
- data/lib/passiveldap.rb +1473 -0
- data/lib/user.rb +423 -0
- data/tests/README +20 -0
- data/tests/scheme_adam.ldif +19 -0
- data/tests/tc_attributes.rb +87 -0
- data/tests/tc_class.rb +77 -0
- data/tests/tc_instance.rb +98 -0
- data/tests/tc_validate.rb +55 -0
- data/tests/ts_all.rb +5 -0
- data/tests/user.rb +39 -0
- metadata +93 -0
data/COPYING
ADDED
@@ -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.
|
data/ChangeLog
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
= PassiveLDAP Changelog
|
2
|
+
|
3
|
+
== TODO
|
4
|
+
=== For 0.2
|
5
|
+
* Locking during saving records, counting records, etc.
|
6
|
+
* Ability to use an ActiveRecord model as an "SQL-cache"
|
7
|
+
* Constraint checkings (id is unique, etc.)
|
8
|
+
|
9
|
+
=== For the future
|
10
|
+
* Ability to use a hash (or something like that) of the distinguished name as the id of a record
|
11
|
+
* More supported types (besides String and Array (of Strings))
|
12
|
+
* More tests (authentications, set_password, etc.)
|
13
|
+
* Implement features that throw ARMethodMissing or ARFeatureMissing
|
14
|
+
* More compatibility with servers other than Active Directory
|
15
|
+
* More compatibility with ActiveRecord
|
16
|
+
|
17
|
+
== PassiveLDAP 0.1
|
18
|
+
* initial release
|
19
|
+
* most parts functional
|
20
|
+
* most of the base functionality is ActiveRecord compatible
|
21
|
+
* uses Net::LDAP 0.0.4, ActiveRecord 2.0.2 and ActiveSupport 2.0.2
|
22
|
+
* uses iconv for Active Directory password changes
|
data/LICENCE
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
PassiveLDAP is copyrighted free software by Zsolt Sz. Sztupak
|
2
|
+
<mail@sztupy.hu>. 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.
|
data/README
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
= PassiveLDAP
|
2
|
+
ActiveRecord and LDAP interoperability support library. It is called
|
3
|
+
PassiveLDAP, because the programmer has to define which attributes
|
4
|
+
s/he will need, and what their type is. After that however the library works
|
5
|
+
as an active way by mapping objects between ruby/rails and the LDAP
|
6
|
+
server.
|
7
|
+
|
8
|
+
The library is currently used internally in an Active Directory environment, but should
|
9
|
+
work using other LDAP servers too. The User class is a real-life example based on this environment.
|
10
|
+
|
11
|
+
Homepage: http://passiveldap.sztupy.hu
|
12
|
+
Copyright: (C) 2008 by Zsolt Sz. Sztup�k
|
13
|
+
|
14
|
+
== LICENCE NOTES
|
15
|
+
Please read the LICENCE[link:files/LICENCE.html] and COPYING[link:files/COPYING.html] files for licensing restrictions on this library. In
|
16
|
+
the simplest terms, this library is available under the same terms as Ruby itself.
|
17
|
+
|
18
|
+
== Requirements
|
19
|
+
Requires Net::LDAP (0.0.4), ActiveRecord (2.0.2) and ActiveSupport (2.0.2)
|
20
|
+
|
21
|
+
== Documentation
|
22
|
+
Check PassiveLDAP for documentation and the User class for an example use.
|
23
|
+
|
24
|
+
== TODO
|
25
|
+
See ChangeLog[link:files/ChangeLog.html]
|
26
|
+
|
27
|
+
== Tests
|
28
|
+
To run them check their readme[link:files/tests/README.html]
|
data/lib/passiveldap.rb
ADDED
@@ -0,0 +1,1473 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_record"
|
3
|
+
require "net/ldap"
|
4
|
+
require "iconv"
|
5
|
+
|
6
|
+
# = PassiveLDAP
|
7
|
+
#
|
8
|
+
# This class is for ActiveRecord <=> LDAP interoparibility, designed so
|
9
|
+
# most of the data can be stored in SQL / ActiveRecord tables, but some data
|
10
|
+
# (usally the User datas) may be stored in an LDAP directory. PassiveLDAP
|
11
|
+
# tries to emulate ActiveRecord as much as possible (like it includes
|
12
|
+
# ActiveRecord::Validation, so you may use those methods
|
13
|
+
# for attribute validations), and extending it with some methods that are
|
14
|
+
# useful when using an LDAP directory. This library can be thought of
|
15
|
+
# a high level library on top of Net::LDAP
|
16
|
+
#
|
17
|
+
# PassiveLDAP has some "advanced" features. See PassiveLDAP::Base#set_protection_level, PassiveLDAP::Base#set_password and PassiveLDAP::Base#passive_ldap[:default_array_separator]
|
18
|
+
#
|
19
|
+
# == Usage
|
20
|
+
#
|
21
|
+
# Create a subclass of PassiveLDAP, then use the following macros in the subclass' body
|
22
|
+
# to set the connection, and the attributes of the objects: PassiveLDAP::Base#passive_ldap and PassiveLDAP::Base#passive_ldap_attr.
|
23
|
+
#
|
24
|
+
# In other aspects PassiveLDAP tries to emulate ActiveRecord, so you may check
|
25
|
+
# it's documentation too. Methods marked with <b>AR</b> are methods used in ActiveRecord too,
|
26
|
+
# and they are usually compatible with AR (or they raise ARFeatureMissing or ARMethodMissing)
|
27
|
+
#
|
28
|
+
# == Example
|
29
|
+
#
|
30
|
+
# the User class is a real-life example of the usage of PassiveLDAP.
|
31
|
+
#
|
32
|
+
# check the documentation of PassiveLDAP::Base#passive_ldap and PassiveLDAP::Base#passive_ldap_attr too
|
33
|
+
#
|
34
|
+
# == ActiveRecord compatibility
|
35
|
+
#
|
36
|
+
# PassiveLDAP mixes-in some of the modules that ActiveRecord::Base uses. Things that are somehow tested:
|
37
|
+
# * Validations: #validates_presence_of and #validates_format_of does work, and should other ones too, except
|
38
|
+
# #validates_uniqueness_of, because it depends on SQL. PassiveLDAP has a new validation scheme:
|
39
|
+
# #validates_format_of_each, which will do a #validates_format_of for each element of a multi-valued
|
40
|
+
# attribute.
|
41
|
+
# * Reflections: the Rails 1.2.x dynamic scaffold (after some modifications so it will work with Rails 2.0.2)
|
42
|
+
# works with PassiveLDAP, but ActiveScaffold doesn't (even after some tinkering. Don't know why, it will only
|
43
|
+
# show the number of records, and the same amount of bars)
|
44
|
+
#
|
45
|
+
# The other ones (like Aggregations, Callbacks, Observers, etc.) may work too (or may raise lots of errors), but
|
46
|
+
# are untested
|
47
|
+
#
|
48
|
+
# PassiveLDAP should work as a "belongs_to" in an ActiveRecord
|
49
|
+
# example:
|
50
|
+
# class User < PassiveLDAP::Base
|
51
|
+
# #config
|
52
|
+
# end
|
53
|
+
# class Account < ActiveRecord::Base
|
54
|
+
# belongs_to :user, :class_name => "User", :foreign_key => "user_id"
|
55
|
+
# # some more config
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# after this you may say something like:
|
59
|
+
# an_account.user.cn
|
60
|
+
#
|
61
|
+
# Don't use "eager loading" as that will of course not work! (it is SQL specific)
|
62
|
+
#
|
63
|
+
# Setting #has_one or #has_many in PassiveLDAP is untested (likely to fail)
|
64
|
+
# example:
|
65
|
+
# class User < PassiveLDAP::Base
|
66
|
+
# has_one :account
|
67
|
+
# # more config
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# == Disclaimer
|
71
|
+
#
|
72
|
+
# The library is in an early alpha-stage. Use at your own risk.
|
73
|
+
#
|
74
|
+
# Bug-fixes and feature-additions sent to my email adress are welcome!
|
75
|
+
module PassiveLDAP
|
76
|
+
|
77
|
+
# some type constants that may be used as the <tt>:type</tt> parameter of an attribute declaration
|
78
|
+
#
|
79
|
+
# Currently available types:
|
80
|
+
# * ANSI_Date, which will convert an ANSI date number to a human readable time string.
|
81
|
+
# This type is read only, so there is only a <tt>:from</tt> conversion specified here. There may be a few hours of difference,
|
82
|
+
# because of time zone errors. This should be fixed.
|
83
|
+
# * Epoch_Date, which will convert an epoch date to a human readable time string.
|
84
|
+
module Types
|
85
|
+
#--
|
86
|
+
# 116444700000000000: miliseconds between 1601-01-01 and 1970-01-01. Or something like that
|
87
|
+
# No error checking. Will throw errors at dates like "infinity"
|
88
|
+
#
|
89
|
+
# RDoc has an error parsing the document if the constant Hash below
|
90
|
+
# is split through separate lines, and if I use do..end instead of { }.
|
91
|
+
# The parsing goes wrong even if the Hash contains the word "end". That is
|
92
|
+
# why I ende up using the ?: operator and putting the whole value into one line
|
93
|
+
#++
|
94
|
+
ANSI_Date = { :from => Proc.new { |s| (s.nil? or s=="" or s=="0") ? "unused" : Time.at((Integer(s) - 116444700000000000) / 10000000).to_s } }
|
95
|
+
Epoch_Date = { :from => Proc.new { |s| Time.at(s.to_i).to_s } }
|
96
|
+
end
|
97
|
+
|
98
|
+
=begin
|
99
|
+
###########################################################
|
100
|
+
# Exception definitions
|
101
|
+
###########################################################
|
102
|
+
=end
|
103
|
+
|
104
|
+
# superclass of the PassiveLDAP exceptions
|
105
|
+
class PassiveLDAPError < Exception #:doc:
|
106
|
+
end
|
107
|
+
|
108
|
+
# Raised when the record is not found
|
109
|
+
class RecordNotFound < PassiveLDAPError
|
110
|
+
end
|
111
|
+
|
112
|
+
# Raised when the record is not saved
|
113
|
+
class RecordNotSaved < PassiveLDAPError
|
114
|
+
end
|
115
|
+
|
116
|
+
# Raised when the assignment fails (like the attribute does not exist)
|
117
|
+
class AttributeAssignmentError < PassiveLDAPError
|
118
|
+
end
|
119
|
+
|
120
|
+
# Raised when the distinguished name does not exist when the item is saved, or when someone tries to change the dn of an
|
121
|
+
# already existing object
|
122
|
+
class DistinguishedNameException < PassiveLDAPError
|
123
|
+
end
|
124
|
+
|
125
|
+
# Thrown in case the connection fails
|
126
|
+
class ConnectionError < PassiveLDAPError
|
127
|
+
end
|
128
|
+
|
129
|
+
# Thrown if a method present in ActiveRecord is called but it is not implemented in PassiveLDAP (but should be sometime)
|
130
|
+
class ARMethodMissing < PassiveLDAPError
|
131
|
+
end
|
132
|
+
|
133
|
+
# Thrown if a method doesn't implement all features what it should if it were an ActiveRecord, and such a feature is used
|
134
|
+
class ARFeatureMissing < PassiveLDAPError
|
135
|
+
end
|
136
|
+
|
137
|
+
# Base class. See the documentation of #passive_ldap and #passive_ldap_attr
|
138
|
+
class Base
|
139
|
+
VERSION = "0.1"
|
140
|
+
|
141
|
+
# <b>AR</b> Determines whether to use Time.local (using <tt>:local)</tt> or Time.utc (using <tt>:utc)</tt> when pulling dates and times from the database.
|
142
|
+
# This is set to <tt>:local</tt> by default.
|
143
|
+
cattr_accessor :default_timezone, :instance_writer => false
|
144
|
+
@@default_timezone = :local
|
145
|
+
|
146
|
+
class << self
|
147
|
+
=begin
|
148
|
+
###########################################################
|
149
|
+
# public PassiveLDAP-only class methods
|
150
|
+
###########################################################
|
151
|
+
=end
|
152
|
+
|
153
|
+
# gets the hash set with #passive_ldap
|
154
|
+
def settings
|
155
|
+
read_inheritable_attribute(:connection)
|
156
|
+
end
|
157
|
+
|
158
|
+
# gets the attributes hash set with #passive_ldap_attr (excluding hidden values)
|
159
|
+
def attrs
|
160
|
+
read_inheritable_attribute(:attrs)
|
161
|
+
end
|
162
|
+
|
163
|
+
# gets the attributes hash set with #passive_ldap_attr (including hidden values)
|
164
|
+
def attrs_all
|
165
|
+
read_inheritable_attribute(:attr_orig)
|
166
|
+
end
|
167
|
+
|
168
|
+
# gets the attribute_ldap_server_name=>attribute_passive_ldap_name hash
|
169
|
+
def attr_mapto
|
170
|
+
read_inheritable_attribute(:mapto)
|
171
|
+
end
|
172
|
+
|
173
|
+
# gets the attribute_passive_ldap_name=>attribute_ldap_server_name hash
|
174
|
+
def attr_mapfrom
|
175
|
+
read_inheritable_attribute(:mapfrom)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Binds to the directory with the username and password given. Password may be a Proc object,
|
179
|
+
# see the documentation of Net::LDAP#bind
|
180
|
+
#
|
181
|
+
# Will return true if the bind is sucesful, and will raise a ConnectionError with the message returned from the server
|
182
|
+
# if the bind fails
|
183
|
+
#
|
184
|
+
# If password and username is nil, bind will try to bind with the default connection parameters
|
185
|
+
#
|
186
|
+
# Beware! Password is the first parameter!
|
187
|
+
def bind(password = nil, username = nil)
|
188
|
+
ldap = initialize_ldap_con
|
189
|
+
ldap.authenticate(username,password) if password
|
190
|
+
ldap.bind
|
191
|
+
raise ConnectionError, ldap.get_operation_result.message unless ldap.get_operation_result.code == 0
|
192
|
+
true
|
193
|
+
end
|
194
|
+
|
195
|
+
=begin
|
196
|
+
###########################################################
|
197
|
+
# public ActiveRecord compatible class methods
|
198
|
+
###########################################################
|
199
|
+
=end
|
200
|
+
|
201
|
+
# <b>AR</b> Returns an array of the generated methods
|
202
|
+
def generated_methods
|
203
|
+
@generated_methods ||= Set.new
|
204
|
+
end
|
205
|
+
|
206
|
+
# <b>AR</b> Returns true - attribute methods are generated in initalize
|
207
|
+
def generated_methods?
|
208
|
+
true
|
209
|
+
end
|
210
|
+
|
211
|
+
# <b>AR</b> always returns the number of records.
|
212
|
+
# Should be changed to something more intelligent
|
213
|
+
#
|
214
|
+
# Doesn't raise ARFeatureMissing yet
|
215
|
+
def count(*args)
|
216
|
+
find(:all).length
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
# <b>AR</b> returns an array of the attribute names as strings (if mapped then it will return the mapped name)
|
221
|
+
def column_names
|
222
|
+
unless @column_names
|
223
|
+
@column_names = ["id"]
|
224
|
+
attrs.each { |key,value|
|
225
|
+
@column_names << value[:name].to_s if key != settings[:id_attribute]
|
226
|
+
}
|
227
|
+
end
|
228
|
+
@column_names
|
229
|
+
end
|
230
|
+
|
231
|
+
# <b>AR</b> returns an array of the columns as ActiveRecord::ConnectionAdapters::Column
|
232
|
+
#
|
233
|
+
# The id is 'int(8)' the multi-valued attributes are 'text', all others are 'varchar'
|
234
|
+
def columns
|
235
|
+
unless @columns
|
236
|
+
@columns = self.column_names.collect { |e|
|
237
|
+
if e == "id" then
|
238
|
+
i = ActiveRecord::ConnectionAdapters::Column.new("id",'0','int(8)',false)
|
239
|
+
i.primary = true
|
240
|
+
else
|
241
|
+
i = ActiveRecord::ConnectionAdapters::Column.new(e,'',attrs[attr_mapfrom[e.to_sym]][:multi_valued]?'text':'varchar',true)
|
242
|
+
end
|
243
|
+
i
|
244
|
+
}
|
245
|
+
end
|
246
|
+
@columns
|
247
|
+
end
|
248
|
+
|
249
|
+
# <b>AR</b> returns a hash of column objects. See columns
|
250
|
+
def columns_hash
|
251
|
+
unless @columns_hash
|
252
|
+
a = self.columns
|
253
|
+
@columns_hash = {}
|
254
|
+
a.each { |e|
|
255
|
+
@columns_hash[e.name] = e
|
256
|
+
}
|
257
|
+
end
|
258
|
+
@columns_hash
|
259
|
+
end
|
260
|
+
|
261
|
+
# <b>AR</b> return the array of column objects without the id column
|
262
|
+
def content_columns
|
263
|
+
a = columns
|
264
|
+
a.delete_if { |e| e.name == "id" }
|
265
|
+
a
|
266
|
+
end
|
267
|
+
|
268
|
+
# <b>AR</b> Creates an object (or multiple objects) and saves it to the database, if validations pass. The resulting object is
|
269
|
+
# returned whether the object was saved successfully to the database or not.
|
270
|
+
#
|
271
|
+
# The attributes parameter can be either be a Hash or an Array of Hashes. These Hashes describe the attributes on
|
272
|
+
# the objects that are to be created.
|
273
|
+
def create(attributes = nil)
|
274
|
+
if attributes.nil? then
|
275
|
+
a = new
|
276
|
+
a.save
|
277
|
+
a
|
278
|
+
else
|
279
|
+
attributes = [attributes] unless attributes.kind_of?(Array)
|
280
|
+
c = []
|
281
|
+
attributes.each { |b|
|
282
|
+
b[:id] ||= nil
|
283
|
+
a = new(b[:id])
|
284
|
+
b.each { |key,value|
|
285
|
+
if key!=:id then
|
286
|
+
a[key] = value
|
287
|
+
end
|
288
|
+
}
|
289
|
+
a.save
|
290
|
+
c << a
|
291
|
+
}
|
292
|
+
if attributes.length==1 then
|
293
|
+
c[0]
|
294
|
+
else
|
295
|
+
c
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# <b>AR</b> deletes the record. Object will be instantiated
|
301
|
+
def delete(id)
|
302
|
+
a = new(id)
|
303
|
+
a.destroy
|
304
|
+
end
|
305
|
+
|
306
|
+
# <b>AR</b> not implemented. Raises ARMethodMissing
|
307
|
+
def delete_all(conditions = nil)
|
308
|
+
raise ARMethodMissing, "ARMethodMissing: delete_all"
|
309
|
+
end
|
310
|
+
|
311
|
+
# <b>AR</b> same as delete
|
312
|
+
def destroy(id)
|
313
|
+
delete(id)
|
314
|
+
end
|
315
|
+
|
316
|
+
# <b>AR</b> not implemented. Raises ARMethodMissing
|
317
|
+
def destroy_all(conditions = nil)
|
318
|
+
raise ARMethodMissing, "ARMethodMissing: destroy_all"
|
319
|
+
end
|
320
|
+
|
321
|
+
# <b>AR</b> checks whether the given id, or an object that satisfies the given Net::LDAP::Filter exist in the directory
|
322
|
+
#
|
323
|
+
# will throw ARFeatureMissing if id_or_filter is not an integer or a Filter
|
324
|
+
def exists?(id_or_filter)
|
325
|
+
raise ARFeatureMissing, "id_or_filter must be an id or a filter" unless id_or_filter.kind_of?(Integer) or (id_or_filter.kind_of?(String) and id_or_filter.to_i.to_s == id_or_filter) or id_or_filter.kind_of?(Net::LDAP::Filter)
|
326
|
+
begin
|
327
|
+
if id_or_filter.kind_of?(Net::LDAP::Filter) then
|
328
|
+
find(:first,id_or_filter)
|
329
|
+
else
|
330
|
+
find(id_or_filter)
|
331
|
+
end
|
332
|
+
rescue RecordNotFound
|
333
|
+
return false
|
334
|
+
end
|
335
|
+
true
|
336
|
+
end
|
337
|
+
|
338
|
+
# <b>AR</b> find a user defined by it's ID and return the object.
|
339
|
+
# If it is not found in the database it will raise RecordNotFound
|
340
|
+
#
|
341
|
+
# If you pass the <tt>:all</tt> symbol as parameter, it will return an array with all objects in the directory. If
|
342
|
+
# no object is found it will return an empty array
|
343
|
+
#
|
344
|
+
# If you pass the <tt>:first</tt> symbol as parameter, it will return the first object in the directory
|
345
|
+
#
|
346
|
+
# the optional filter parameter is used to join a new filter to the default one. The filter parameter is only
|
347
|
+
# used in <tt>:all</tt> and <tt>:first</tt> searches
|
348
|
+
#
|
349
|
+
# will throw ARFeatureMissing if passed a Hash or an Array instead of a Net::LDAP::Filter, or if the first parameter
|
350
|
+
# is not an id, or one the following symbols: <tt>:all</tt>, <tt>:first</tt>
|
351
|
+
#
|
352
|
+
# Currently it will allow Hash filters, if all of the Hash parameters are nil. This is because doing so belongs_to
|
353
|
+
# relations will work.
|
354
|
+
def find(user, filter = nil)
|
355
|
+
raise ARFeatureMissing, "User must be a number, :all or :first. Supplied was #{filter.inspect}" unless user.kind_of?(Integer) or user == :all or user == :first or (user.kind_of?(String) and user.to_i.to_s == user)
|
356
|
+
if filter.kind_of?(Hash) then
|
357
|
+
testf = true
|
358
|
+
filter.each { |key,value|
|
359
|
+
testf = false unless value.nil?
|
360
|
+
}
|
361
|
+
filter = nil if testf
|
362
|
+
end
|
363
|
+
raise ARFeatureMissing, "Filter must be a Net::LDAP::Filter or nil. Supplied was #{filter.inspect}" unless filter.nil? or filter.kind_of?(Net::LDAP::Filter)
|
364
|
+
#filter = nil unless filter.kind_of?(Net::LDAP::Filter)
|
365
|
+
if user == :all or user == :first then
|
366
|
+
a = []
|
367
|
+
ldap = self.initialize_ldap_con
|
368
|
+
if filter then
|
369
|
+
filter = filter & self.settings[:multiple_record_filter].call(self)
|
370
|
+
else
|
371
|
+
filter = self.settings[:multiple_record_filter].call(self)
|
372
|
+
end
|
373
|
+
alreadygot = false
|
374
|
+
ldap.search( :return_result => false, :scope => self.settings[:record_scope], :base => self.settings[:record_base], :filter => filter ) do |entry|
|
375
|
+
eval "a << self.new(entry.#{self.settings[:id_attribute].id2name}[0].to_i)" unless user == :first and alreadygot
|
376
|
+
alreadygot = true
|
377
|
+
end
|
378
|
+
raise ConnectionError, ldap.get_operation_result.message unless ldap.get_operation_result.code == 0
|
379
|
+
if user == :all then
|
380
|
+
a
|
381
|
+
elsif a == [] then
|
382
|
+
raise PassiveLDAP::RecordNotFound
|
383
|
+
else
|
384
|
+
a[0]
|
385
|
+
end
|
386
|
+
else
|
387
|
+
a = self.new(user)
|
388
|
+
if a.exists_in_directory then
|
389
|
+
a
|
390
|
+
else
|
391
|
+
raise PassiveLDAP::RecordNotFound
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# <b>AR</b> returns a humanized attribute name
|
397
|
+
def human_attribute_name(attribute_key_name)
|
398
|
+
attribute_key_name.humanize
|
399
|
+
end
|
400
|
+
|
401
|
+
# <b>AR</b> returns a string like "<tt>User id:integer name:string mail:text</tt>" multi-valued attributes will be text
|
402
|
+
def inspect()
|
403
|
+
a = column_names
|
404
|
+
b = self.name
|
405
|
+
a.each { |e|
|
406
|
+
if e == "id" then
|
407
|
+
b = b + " id:integer"
|
408
|
+
else
|
409
|
+
if attrs[attr_mapfrom[e.to_sym]][:multi_valued] then
|
410
|
+
b = b + " #{e}:text"
|
411
|
+
else
|
412
|
+
b = b + " #{e}:string"
|
413
|
+
end
|
414
|
+
end
|
415
|
+
}
|
416
|
+
b
|
417
|
+
end
|
418
|
+
|
419
|
+
# <b>AR</b> returns <tt>:id</tt>
|
420
|
+
def primary_key
|
421
|
+
:id
|
422
|
+
end
|
423
|
+
|
424
|
+
# <b>AR</b> not implemented. Will raise ARMethodMissing
|
425
|
+
def serialize(attr_name, class_name = Object)
|
426
|
+
raise ARMethodMissing, "ARMethodMissing: serialize"
|
427
|
+
end
|
428
|
+
|
429
|
+
# <b>AR</b> not implemented. Will raise ARMethodMissing
|
430
|
+
def serialized_attributes
|
431
|
+
raise ARMethodMissing, "ARMethodMissing: serialized_attributes"
|
432
|
+
end
|
433
|
+
|
434
|
+
# <b>AR</b> will return the name of the class
|
435
|
+
def table_name
|
436
|
+
self.name
|
437
|
+
end
|
438
|
+
|
439
|
+
# <b>AR</b> Updates an object or objects (if passed an Array) with the attributes given. Uses save!
|
440
|
+
def update(id, attributes)
|
441
|
+
id = [id] unless id.kind_of?(Array)
|
442
|
+
attributes = [attributes] unless attributes.kind_of?(Array)
|
443
|
+
if id.length != attributes.length then
|
444
|
+
raise PassiveLDAPError, "Argument numbers don't mach"
|
445
|
+
end
|
446
|
+
c = []
|
447
|
+
id.each_index { |v|
|
448
|
+
a = new(id[v])
|
449
|
+
a.update_attributes(attributes[v])
|
450
|
+
c << a
|
451
|
+
}
|
452
|
+
id.length==1 ? c[0] : c
|
453
|
+
end
|
454
|
+
|
455
|
+
# <b>AR</b> not implemented. Will raise ARMethodMissing
|
456
|
+
def update_all(updates, conditions = nil, options = {})
|
457
|
+
raise ARMethodMissing, "ARMethodMissing: update_all"
|
458
|
+
end
|
459
|
+
|
460
|
+
# <b>AR</b> not implemented. Will raise ARMethodMissing
|
461
|
+
def update_counters(id,counters)
|
462
|
+
raise ARMethodMissing, "ARMethodMissing: update_counters"
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
=begin
|
467
|
+
###########################################################
|
468
|
+
# public PassiveLDAP-only instance methods
|
469
|
+
###########################################################
|
470
|
+
=end
|
471
|
+
|
472
|
+
# Bind to the directory to check whether the credentials are right or not. If there are no parameters
|
473
|
+
# specified bind will do the following:
|
474
|
+
# * If the actual protection_level is 0 it will bind with the default connection
|
475
|
+
# * If the level is 1 it will bind with the dn of the record and the password, that is set with
|
476
|
+
# #set_protection_level
|
477
|
+
# * If the level is above 2 it will bind with the dn and password set with #set_protection_level
|
478
|
+
#
|
479
|
+
# Parameters may be used to set the dn and the password used to bind to the directory. Beware!
|
480
|
+
# The first parameter is the password! You may omit the username, in which case the
|
481
|
+
# dn of the record will be used to bind to the directory
|
482
|
+
#
|
483
|
+
# bind will return true if the connection is succesful and will raise a ConnectionError with
|
484
|
+
# a message from the server if the authentication fails
|
485
|
+
def bind(password = nil, username = nil)
|
486
|
+
if password then
|
487
|
+
ldap = self.class.initialize_ldap_con
|
488
|
+
if username then
|
489
|
+
ldap.authenticate(username,password)
|
490
|
+
else
|
491
|
+
ldap.authenticate(dn,password)
|
492
|
+
end
|
493
|
+
ldap.bind
|
494
|
+
raise ConnectionError, ldap.get_operation_result.message unless ldap.get_operation_result.code == 0
|
495
|
+
else
|
496
|
+
ldap = initialize_ldap_con
|
497
|
+
ldap.bind
|
498
|
+
raise ConnectionError, ldap.get_operation_result.message unless ldap.get_operation_result.code == 0
|
499
|
+
end
|
500
|
+
true
|
501
|
+
end
|
502
|
+
|
503
|
+
# changes the password of the record.
|
504
|
+
#
|
505
|
+
# Currently method may only be :active_directory
|
506
|
+
#
|
507
|
+
# For options check #set_password_ad
|
508
|
+
#
|
509
|
+
# will return false if unsuccesful, adding the response from the server to the errors list
|
510
|
+
def set_password(newpass, method, options = nil)
|
511
|
+
set_password!
|
512
|
+
rescue RecordNotSaved
|
513
|
+
return false
|
514
|
+
else
|
515
|
+
return true
|
516
|
+
end
|
517
|
+
|
518
|
+
# same as set_password but will raise a RecordNotSaved exception in unsuccesful
|
519
|
+
def set_password!(newpass, method, options = nil)
|
520
|
+
if method == :active_directory then
|
521
|
+
set_password_ad(newpass, options)
|
522
|
+
else
|
523
|
+
raise ARFeatureMissing, "Only AD password changes supported!"
|
524
|
+
end
|
525
|
+
rescue Exception => e
|
526
|
+
@errors.add_to_base(e)
|
527
|
+
raise
|
528
|
+
end
|
529
|
+
|
530
|
+
# Attributes may have different protection levels. Protection level means, that some attributes
|
531
|
+
# may only be changed by privileged users. Level 0 means that the attribute may be changed by the
|
532
|
+
# main connection. Level 1 means, the attribute can be changed by the owner of the attribute, but cannot
|
533
|
+
# be changed by the main connection. Level 2 and higher level means that the attribute can only be changed
|
534
|
+
# with a user, who has enough privileges.
|
535
|
+
#
|
536
|
+
# For example if PassiveLDAP is used for storing User information,
|
537
|
+
# you might set most of the attributes to level 1 (so the password of the user will be needed to change
|
538
|
+
# those information) and some attributes (such as printAccount, or like) may be set to level 2 or higher, so
|
539
|
+
# only privileged users (like administrators) could change those attributes.
|
540
|
+
#
|
541
|
+
# the method has 3 paramteres. The first one sets the desired level, the second one is the password of the
|
542
|
+
# user (if the level is greater or equal than 1) and the third one is the username (full dn!) of the
|
543
|
+
# user (if the level is above 1)
|
544
|
+
#
|
545
|
+
# Protection means that when issuing a save method, only those attributes will be saved, that are below
|
546
|
+
# or equal to the protection level set here, the other ones won't be sent to the LDAP server. Of course
|
547
|
+
# you should set the appropriate rights in the server too for maximum security.
|
548
|
+
#
|
549
|
+
# Class methods (like find) will be run with the connection's authenticity information while instance methods will run
|
550
|
+
# with the actual username and password set with set_protection_level
|
551
|
+
#
|
552
|
+
# Beware! the second parameter is the password and the third is the username!
|
553
|
+
def set_protection_level(level = 0, password = nil, username = nil)
|
554
|
+
@protection_level = level
|
555
|
+
@protection_username = username
|
556
|
+
@protection_password = password
|
557
|
+
end
|
558
|
+
|
559
|
+
# gets whether the id is set. Returns always true
|
560
|
+
def id?
|
561
|
+
true
|
562
|
+
end
|
563
|
+
|
564
|
+
# gets the distinguished name of the record. Returns nil if the record is nonexistent in the directory
|
565
|
+
def dn
|
566
|
+
@attributes[:dn]
|
567
|
+
end
|
568
|
+
|
569
|
+
# sets the distinguished name of the record. The dn can only be set when the record is not originated from the directory
|
570
|
+
# (so it is a new record) Otherwise a DistinguishedNameException is raised
|
571
|
+
def dn=(newdn)
|
572
|
+
raise PassiveLDAP::DistinguishedNameException, "DN cannot be changed" unless @oldattr[:dn].nil?
|
573
|
+
@dn = newdn
|
574
|
+
@attributes[:dn]=newdn
|
575
|
+
end
|
576
|
+
|
577
|
+
# returns whether the record is new, or it is originated from the directory
|
578
|
+
#
|
579
|
+
# if it exists it will return the dn of the record, if not it will return nil
|
580
|
+
def exists_in_directory
|
581
|
+
@oldattr[:dn]
|
582
|
+
end
|
583
|
+
|
584
|
+
# gets the original value (the value that was read from the directory, or nil if this is a new record) of an attribute
|
585
|
+
def get_old_attribute(attribute)
|
586
|
+
attribute = attribute.to_sym unless attribute.kind_of?(Symbol)
|
587
|
+
if self.class.attr_mapfrom.has_key?(attribute) then
|
588
|
+
@oldattr[self.class.attr_mapfrom[attribute]]
|
589
|
+
else
|
590
|
+
if attribute == :id then
|
591
|
+
@oldattr[self.settings[:id_attribute]]
|
592
|
+
else
|
593
|
+
raise PassiveLDAP::AttributeAssignmentError, "Attribute #{attribute} does not exist"
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# returns the user id as string
|
599
|
+
def to_s
|
600
|
+
@id.to_s
|
601
|
+
end
|
602
|
+
|
603
|
+
# returns the attrbiute. If it is multi_valued no conversion will be done even if the
|
604
|
+
# array_separator is something else than nil
|
605
|
+
def get_attribute(attribute)
|
606
|
+
attribute = attribute.to_sym unless attribute.kind_of?(Symbol)
|
607
|
+
if self.class.attr_mapfrom.has_key?(attribute) then
|
608
|
+
key = self.class.attr_mapfrom[attribute]
|
609
|
+
if @attributes.has_key?(key) then
|
610
|
+
@attributes[key]
|
611
|
+
else
|
612
|
+
nil
|
613
|
+
end
|
614
|
+
else
|
615
|
+
if attribute == :id then
|
616
|
+
self.id
|
617
|
+
else
|
618
|
+
raise PassiveLDAP::AttributeAssignmentError, "Attribute #{attribute} does not exist"
|
619
|
+
end
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# sets the attribute. If it is multi_valued you need to pass an array even if
|
624
|
+
# the array_separator is set
|
625
|
+
def set_attribute(attribute,value, raise_error_when_readonly = false)
|
626
|
+
attribute = attribute.to_sym unless attribute.kind_of?(Symbol)
|
627
|
+
if self.class.attr_mapfrom.has_key?(attribute) then
|
628
|
+
alt_name = self.class.attr_mapfrom[attribute]
|
629
|
+
if self.class.attrs[alt_name][:read_only]
|
630
|
+
if raise_error_when_readonly then
|
631
|
+
raise PassiveLDAP::AttributeAssignmentError, "Attribute #{attribute} is read-only"
|
632
|
+
else
|
633
|
+
return false
|
634
|
+
end
|
635
|
+
end
|
636
|
+
if self.class.attrs[alt_name][:multi_valued] then
|
637
|
+
raise PassiveLDAP::AttributeAssignmentError, "Array expected, because #{attribute} is multi-valued" unless value.kind_of?(Array)
|
638
|
+
else
|
639
|
+
raise PassiveLDAP::AttributeAssignmentError, "Didn't expect an Array, because #{attribute} is not multi-valued" if value.kind_of?(Array)
|
640
|
+
end
|
641
|
+
eval "@#{attribute.to_s} = value"
|
642
|
+
@attributes[alt_name] = value
|
643
|
+
else
|
644
|
+
if attribute == :id then
|
645
|
+
self.id=value
|
646
|
+
else
|
647
|
+
raise PassiveLDAP::AttributeAssignmentError, "Attribute #{attribute} does not exist"
|
648
|
+
end
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
# sets the array_separator
|
653
|
+
def array_separator(new_sep = nil)
|
654
|
+
@array_separator = new_sep
|
655
|
+
end
|
656
|
+
|
657
|
+
=begin
|
658
|
+
###########################################################
|
659
|
+
# public ActiveRecord compatible instance methods
|
660
|
+
###########################################################
|
661
|
+
=end
|
662
|
+
# <b>AR</b> create a record object and populate it's data from the LDAP directory.
|
663
|
+
# If the record is not found it will create an empty user with that id
|
664
|
+
#
|
665
|
+
# Beware! If userid is nil it will try to guess a new id number using the Proc in #passive_ldap[:new_id]. By default
|
666
|
+
# this guess is not guaranteed to be unique in a multi-threaded application. See #passive_ldap
|
667
|
+
#
|
668
|
+
# the parameter may be a Hash with attributes that are the initial values.
|
669
|
+
def initialize(userid = nil)
|
670
|
+
values = nil
|
671
|
+
if userid.kind_of?(Hash)
|
672
|
+
values = userid.clone
|
673
|
+
values[:id] ||= nil
|
674
|
+
userid = values[:id]
|
675
|
+
end
|
676
|
+
raise ARFeatureMissing, "Id must be a Hash or a number" unless userid.kind_of?(Integer) or (userid.kind_of?(String) and userid.to_i.to_s == userid) or userid.nil?
|
677
|
+
userid = self.class.settings[:new_id].call(self) if userid.nil?
|
678
|
+
@array_separator = self.class.settings[:default_array_separator]
|
679
|
+
@protection_level = 0
|
680
|
+
@protection_username = nil
|
681
|
+
@protection_password = nil
|
682
|
+
@generated_methods = Set.new
|
683
|
+
@dn = nil
|
684
|
+
self.class.attrs.each { |name,value|
|
685
|
+
alt_name = value[:name]
|
686
|
+
eval "@#{alt_name.to_s} = nil"
|
687
|
+
if not self.class.method_defined?(alt_name) then
|
688
|
+
self.class.module_eval <<-EOF
|
689
|
+
def #{alt_name.id2name}
|
690
|
+
read_mapped_attribute(:#{alt_name.to_s})
|
691
|
+
end
|
692
|
+
def #{alt_name.id2name}=(a)
|
693
|
+
write_mapped_attribute(:#{alt_name.to_s},a)
|
694
|
+
end
|
695
|
+
def #{alt_name.id2name}?
|
696
|
+
if @attributes.has_key?(:#{name.to_s}) then
|
697
|
+
unless @attributes[:#{name.to_s}].nil? or @attributes[:#{name.to_s}] == "" or @attributes[:#{name.to_s}] == [] then
|
698
|
+
true
|
699
|
+
else
|
700
|
+
false
|
701
|
+
end
|
702
|
+
else
|
703
|
+
false
|
704
|
+
end
|
705
|
+
end
|
706
|
+
EOF
|
707
|
+
@generated_methods << "#{alt_name.id2name}".to_sym
|
708
|
+
@generated_methods << "#{alt_name.id2name}=".to_sym
|
709
|
+
@generated_methods << "#{alt_name.id2name}?".to_sym
|
710
|
+
end
|
711
|
+
}
|
712
|
+
reload(:id => userid)
|
713
|
+
@errors = ActiveRecord::Errors.new(self)
|
714
|
+
unless values.nil?
|
715
|
+
values[:id] = userid
|
716
|
+
values.each { |key,value|
|
717
|
+
write_mapped_attribute(key,value) unless key == :id
|
718
|
+
}
|
719
|
+
self.id = userid
|
720
|
+
end
|
721
|
+
yield self if block_given?
|
722
|
+
end
|
723
|
+
|
724
|
+
# <b>AR</b> gets the value of the attribute. If the attribute has an alternate name then you have to use it here
|
725
|
+
def [](attribute)
|
726
|
+
read_mapped_attribute(attribute)
|
727
|
+
end
|
728
|
+
|
729
|
+
# <b>AR</b> sets the value of the attribute. If the attribute has an alternate name then you have to use it here
|
730
|
+
def []=(attribute,value)
|
731
|
+
write_mapped_attribute(attribute,value)
|
732
|
+
end
|
733
|
+
|
734
|
+
# <b>AR</b> Returns an array of symbols of the attributes that can be changed; sorted alphabetically
|
735
|
+
def attribute_names()
|
736
|
+
a = self.class.column_names
|
737
|
+
a.collect { |e| e.to_sym }.sort
|
738
|
+
end
|
739
|
+
|
740
|
+
# <b>AR</b> Returns true if the specified attribute has been set by the user
|
741
|
+
# or by a database load and is neither nil nor empty?
|
742
|
+
#
|
743
|
+
# It will always be true for the <tt>:id</tt> and <tt>:dn</tt> attribute (even if the <tt>:dn</tt> is not set)
|
744
|
+
def attribute_present?(attribute)
|
745
|
+
attribute = attribute.to_sym unless attribute.kind_of?(Symbol)
|
746
|
+
return true if attribute == :id or attribute == :dn
|
747
|
+
return false unless attribute_names.include?(attribute)
|
748
|
+
a = self.class.attr_mapfrom[attribute]
|
749
|
+
if @attributes[a].nil? then
|
750
|
+
false
|
751
|
+
elsif @attributes[a].kind_of?(Array) then
|
752
|
+
if @attributes[a] == [] then
|
753
|
+
false
|
754
|
+
else
|
755
|
+
true
|
756
|
+
end
|
757
|
+
elsif @attributes[a].kind_of?(String) then
|
758
|
+
if @attributes[a] == "" then
|
759
|
+
false
|
760
|
+
else
|
761
|
+
true
|
762
|
+
end
|
763
|
+
else
|
764
|
+
true
|
765
|
+
end
|
766
|
+
end
|
767
|
+
|
768
|
+
# <b>AR</b> Returns a hash of all the attributes with their names as keys and clones of their objects as values.
|
769
|
+
#
|
770
|
+
# Options will be ignored (is it used in AR anyway?)
|
771
|
+
def attributes(options = nil)
|
772
|
+
a = { :id => id }
|
773
|
+
@attributes.each { |key,value|
|
774
|
+
v = value
|
775
|
+
v = value.clone if value.duplicable?
|
776
|
+
if self.class.attrs.has_key?(key) then
|
777
|
+
a[self.class.attrs[key][:name]] = v
|
778
|
+
end
|
779
|
+
}
|
780
|
+
a
|
781
|
+
end
|
782
|
+
|
783
|
+
# <b>AR</b> sets multiple attributes at once. if guard_protected_attributes if true only level <tt>settings[:default_protection_level]</tt> attributes will be
|
784
|
+
# changed. guard_protected_attributes may be set to an Integer, indicating which is the maximum level of the attributes
|
785
|
+
# that need to be changed, or to false indicating that all attributes need to be changed
|
786
|
+
def attributes=(new_attributes, guard_protected_attribute = true)
|
787
|
+
guard_protected_attribute = self.class.settings[:default_protection_level] if guard_protected_attribute == true
|
788
|
+
new_attributes.each { |key,value|
|
789
|
+
k = key
|
790
|
+
k = key.to_sym unless key.kind_of?(Symbol)
|
791
|
+
if self.class.attr_mapfrom.has_key?(k) then
|
792
|
+
level = self.class.attrs[self.class.attr_mapfrom[k]][:level]
|
793
|
+
if !guard_protected_attribute or (guard_protected_attribute.kind_of?(Integer) and guard_protected_attribute >= level) then
|
794
|
+
self[k] = value
|
795
|
+
end
|
796
|
+
end
|
797
|
+
}
|
798
|
+
end
|
799
|
+
|
800
|
+
# <b>AR</b> not implemented. Raises ARMethodMissing
|
801
|
+
def clone
|
802
|
+
raise ARMethodMissing, "ARMethodMissing: clone"
|
803
|
+
end
|
804
|
+
|
805
|
+
# <b>AR</b> returns the column object of the named attribute
|
806
|
+
def column_for_attribute(name)
|
807
|
+
self.class.columns_hash[name.to_s]
|
808
|
+
end
|
809
|
+
|
810
|
+
# <b>AR</b> deletes the record in the directory and freezes the object
|
811
|
+
def destroy
|
812
|
+
ldap = initialize_ldap_con
|
813
|
+
ldap.delete(:dn => dn)
|
814
|
+
raise ConnectionError, ldap.get_operation_result.message unless ldap.get_operation_result.code == 0
|
815
|
+
freeze
|
816
|
+
end
|
817
|
+
|
818
|
+
# <b>AR</b> gets the id of the record
|
819
|
+
def id
|
820
|
+
@attributes[self.class.settings[:id_attribute]]
|
821
|
+
end
|
822
|
+
|
823
|
+
# <b>AR</b> sets the id of the record
|
824
|
+
def id=(a)
|
825
|
+
raise PassiveLDAP::AttributeAssignmentError, "Id must be an integer" unless a.kind_of?(Integer) or (a.kind_of?(String) and a.to_i.to_s == a)
|
826
|
+
@attributes[self.class.settings[:id_attribute]] = a
|
827
|
+
@id = a
|
828
|
+
end
|
829
|
+
|
830
|
+
# <b>AR</b> Returns the contents of the record as a string
|
831
|
+
#
|
832
|
+
# should be nicer
|
833
|
+
def inspect
|
834
|
+
"#{self.class.name}: #{attributes.inspect}"
|
835
|
+
end
|
836
|
+
|
837
|
+
# <b>AR</b> Returns true if this object hasn't been saved yet - that is, a record for the object doesn't exist in the directory yet.
|
838
|
+
def new_record?
|
839
|
+
if exists_in_directory then
|
840
|
+
false
|
841
|
+
else
|
842
|
+
true
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
# <b>AR</b> reloads the data from the directory. If the record does not exists it will erase all attributes and set id to the
|
847
|
+
# old value. If the record was acquired from the directory and the id was changed the old id will be used to load the data,
|
848
|
+
# but the id will be set to the new one after the data has benn loaded. This may be changed with the <tt>:newid</tt> option
|
849
|
+
#
|
850
|
+
# options may be
|
851
|
+
# * <tt>:id</tt>: set the id to this new value. If set the <tt>:newid</tt> attribute won't be checked
|
852
|
+
# * <tt>:oldattr</tt>: set to true if you want to load the attributes only into the @oldattr variable, but not into the @attributes
|
853
|
+
# * <tt>:newid</tt>: set to true if you want to load the new id's data (if you changed the id of the data before reloading)
|
854
|
+
def reload(options = nil)
|
855
|
+
options = {} if options.nil?
|
856
|
+
id_set = true
|
857
|
+
options[:newid] ||= false
|
858
|
+
options[:oldattr] ||= false
|
859
|
+
unless options.has_key?(:id) then
|
860
|
+
id_set = false
|
861
|
+
new_id = id
|
862
|
+
options[:id] ||= id
|
863
|
+
options[:id] = @oldattr[self.class.settings[:id_attribute]] unless options[:newid]
|
864
|
+
end
|
865
|
+
@oldattr = {}
|
866
|
+
ldap = self.class.initialize_ldap_con
|
867
|
+
entry = ldap.search( :base => self.class.settings[:record_base], :scope => self.class.settings[:record_scope], :filter => self.class.settings[:single_record_filter].call(self.class,options[:id].to_s) )
|
868
|
+
raise ConnectionError, ldap.get_operation_result.message unless ldap.get_operation_result.code == 0
|
869
|
+
if entry and entry != [] then
|
870
|
+
@oldattr[:dn] = entry[0].dn.downcase
|
871
|
+
entry[0].each { |name, values|
|
872
|
+
if self.class.attrs_all.has_key?(name) then
|
873
|
+
if self.class.attrs_all[name][:multi_valued] then
|
874
|
+
@oldattr[name] = values
|
875
|
+
else
|
876
|
+
@oldattr[name] = values[0]
|
877
|
+
end
|
878
|
+
end
|
879
|
+
}
|
880
|
+
else
|
881
|
+
@oldattr[:dn] = nil
|
882
|
+
end
|
883
|
+
@oldattr[self.class.settings[:id_attribute]] = options[:id]
|
884
|
+
unless options[:oldattr] then
|
885
|
+
@attributes = @oldattr.clone
|
886
|
+
@dn = @attributes[:dn]
|
887
|
+
@attributes.each { |key,value|
|
888
|
+
if self.class.attrs.has_key?(key) then
|
889
|
+
alt_name = self.class.attrs[key][:name]
|
890
|
+
eval "@#{alt_name.to_s} = value"
|
891
|
+
end
|
892
|
+
}
|
893
|
+
@id = options[:id]
|
894
|
+
if !id_set and !options[:newid] then
|
895
|
+
@attributes[self.class.settings[:id_attribute]] = new_id
|
896
|
+
@id = new_id
|
897
|
+
end
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
# <b>AR</b> needed by ActiveRecord::Callbacks
|
902
|
+
def respond_to_without_attributes?(method, include_priv=false)
|
903
|
+
method_name = method.to_s
|
904
|
+
method_name.chomp!("?")
|
905
|
+
method_name.chomp!("!")
|
906
|
+
return false if self.class.attr_mapfrom.has_key?(method_name.to_sym)
|
907
|
+
respond_to?(method, include_priv)
|
908
|
+
end
|
909
|
+
|
910
|
+
# <b>AR</b> Saves the changes back to the LDAP server.
|
911
|
+
# Only the changes will be saved, and only those attributes will
|
912
|
+
# be saved whose protection level is less or equal than the actual
|
913
|
+
# protection level.
|
914
|
+
#
|
915
|
+
# Attributes with default values will get their new values calculated
|
916
|
+
#
|
917
|
+
# The modifications will be sent to server as one modification chunk,
|
918
|
+
# but it depends on the LDAP server whether it will modify the
|
919
|
+
# directory as an atomic transaction. If an error occurs you should
|
920
|
+
# check whether the directory remained in a consistent state. See Net::LDAP#modify
|
921
|
+
# for more information
|
922
|
+
#
|
923
|
+
# Before saving the attributes are loaded from the server to check what has changed.
|
924
|
+
# Between the loading and the saving other threads may modify the directory so be aware of
|
925
|
+
# this.
|
926
|
+
#
|
927
|
+
# TODO: some kind of locking system
|
928
|
+
#
|
929
|
+
# Returns false if an error occurs.
|
930
|
+
def save
|
931
|
+
save!
|
932
|
+
rescue RecordNotSaved => e
|
933
|
+
return false
|
934
|
+
rescue ActiveRecord::RecordInvalid
|
935
|
+
return false
|
936
|
+
else
|
937
|
+
return true
|
938
|
+
end
|
939
|
+
|
940
|
+
# <b>AR</b> saves the record but will raise a RecordNotSaved with the cause of the failure if unsuccesful. See save
|
941
|
+
def save!
|
942
|
+
create_or_update
|
943
|
+
rescue RecordNotSaved => e
|
944
|
+
@errors.add_to_base(e)
|
945
|
+
raise
|
946
|
+
end
|
947
|
+
|
948
|
+
# <b>AR</b> updates a single attribute and saves the record. See ActiveRecord::Base#update_attribute
|
949
|
+
def update_attribute(name, value)
|
950
|
+
self[name] = value
|
951
|
+
save
|
952
|
+
end
|
953
|
+
|
954
|
+
# <b>AR</b> updates multiple attributes and saves the record. See update_attribute.
|
955
|
+
def update_attributes(attributes)
|
956
|
+
update_attributes!(attributes)
|
957
|
+
rescue RecordNotFound
|
958
|
+
return false
|
959
|
+
else
|
960
|
+
return true
|
961
|
+
end
|
962
|
+
|
963
|
+
# <b>AR</b> see update_attributes. Uses save! instead of save
|
964
|
+
def update_attributes!(attributes)
|
965
|
+
self.attributes=(attributes)
|
966
|
+
save!
|
967
|
+
end
|
968
|
+
|
969
|
+
#########
|
970
|
+
protected
|
971
|
+
#########
|
972
|
+
|
973
|
+
class << self
|
974
|
+
=begin
|
975
|
+
###########################################################
|
976
|
+
# protected PassiveLDAP-only class methods
|
977
|
+
###########################################################
|
978
|
+
=end
|
979
|
+
|
980
|
+
# sets the connection and record attributes that are used.
|
981
|
+
# The parameter is a hash with the following options. If there are parameters missing, then the default values will be
|
982
|
+
# used instead of them.
|
983
|
+
#
|
984
|
+
# * <tt>:connection</tt>: The <tt>:connection</tt> is a hash that will be passed without modification to Net::LDAP. The default value is
|
985
|
+
# to connect to localhost on port 389 as anonymous.
|
986
|
+
# * <tt>:id_attribute</tt>: The <tt>:id_attribute</tt> is a symbol, that tells PassiveLDAP which attribute is used as the id of a record. This attribute must be an integer attribute
|
987
|
+
# and it must be unique. (Although there are no constraint checkings yet)
|
988
|
+
# * <tt>:multiple_record_filter</tt>: The <tt>:multiple_record_filter</tt> is a Proc object with one argument, that should return a Net::LDAP::Filter object that will return all
|
989
|
+
# the appropriate records in the directory. The default value is a filter that filters out the object based whether their attribute that is sat in <tt>:id_attribute</tt>
|
990
|
+
# is set. The first argument of the block will be set to the caller PassiveLDAP object.
|
991
|
+
# * <tt>:single_record_filter</tt>: The <tt>:single_record_filter</tt> is a Proc object with two arguments: the caller PassiveLDAP object and an id number. The corresponding
|
992
|
+
# block should return a filter that will filter out the record which has the appropriate id. The default value of this argument is
|
993
|
+
# to check whether the attribute set with <tt>:id_attribute</tt> is equal to the specified id number.
|
994
|
+
# * <tt>:record_base</tt>: The <tt>:record_base</tt> is a String that is set to the base of the records. The default value is "ou=users,dc=com"
|
995
|
+
# * <tt>:record_scope</tt>: The <tt>:record_scope</tt> is a Net::LDAP::Scope object that sets the scope of the records according to the <tt>:record_base.</tt> The default value is Net::LDAP::SearchScope_SingleLevel
|
996
|
+
# * <tt>:new_id</tt>: The <tt>:new_id</tt> is a Proc object that will return an integer which should be an id that is not present in the directory. The default value is 10000 + count*5 + rand(5)
|
997
|
+
# which is not really safe
|
998
|
+
# * <tt>:default_array_separator</tt>: sets the string that will separate the multi-valued attributes if they are converted to string. Set
|
999
|
+
# to nil if you don't want this conversion. This separator may be set with array_separator in an instance too. If this attribute is
|
1000
|
+
# not nil every attribute setter/getter excluding get_attribute and set_attribute will use a converted string to set/get these attributes.
|
1001
|
+
# If the separator is \n then trailing \r characters will be chomped from the splitted strings.
|
1002
|
+
# * <tt>:default_protection_level</tt>: sets the default level. All attributes added after this is set wil have this default level number, unless
|
1003
|
+
# they explicit specify something else. Default is 0
|
1004
|
+
#
|
1005
|
+
# example (as well as the default values):
|
1006
|
+
# passive_ldap :connection => {:host => "127.0.0.1", :port => "389", :auth => { :method => :anonymous } },
|
1007
|
+
# :id_attribute => :id,
|
1008
|
+
# :multiple_record_filter => Proc.new { |s| Net::LDAP::Filter.eq(s.settings[:id_attribute].id2name,"*") },
|
1009
|
+
# :single_record_filter => Proc.new { |s,id| Net::LDAP::Filter.eq(s.settings[:id_attribute].id2name,id) },
|
1010
|
+
# :record_base => "ou=users,dc=com",
|
1011
|
+
# :record_scope => Net::LDAP::SearchScope_SingleLevel,
|
1012
|
+
# :new_id => Proc.new { |s| 10000 + s.class.count*5 + rand(5) },
|
1013
|
+
# :default_array_separator => nil,
|
1014
|
+
# :default_protection_level => 0
|
1015
|
+
def passive_ldap(connection_attributes)
|
1016
|
+
write_inheritable_hash(:connection, connection_attributes)
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
# Sets the attributes you would like to use. Only the attributes set here, the attribute of the id and the dn attribute
|
1020
|
+
# will be queried from the directory. The id_attribute and dn attributes are used automatically so they
|
1021
|
+
# must not be set here (unless you define the dn attribute hidden with a default_value).
|
1022
|
+
# The id attribute is always mapped to the name <tt>:id</tt> regardless of it's original name.
|
1023
|
+
#
|
1024
|
+
# All attributes will get a getter and a setter method with their respective name (unless a mapping is defined in attribute_map),
|
1025
|
+
# as well as a query method, that queries whether the attribute is set or not. They also get an instance variable with their mapped
|
1026
|
+
# name (although it is only used to write to. Some AR specific methods may read the attributes data from instance variables. PassiveLDAP
|
1027
|
+
# stores the attributes in the @attributes Hash)
|
1028
|
+
#
|
1029
|
+
# By default there are no attributes defined. Multiple calls of this method will result in the union of the attributes
|
1030
|
+
#
|
1031
|
+
# The attributes are set as a Hash, where the key is the name of the attribute and the value is a Hash with the following options:
|
1032
|
+
# * <tt>:type</tt>: defines a Hash with a <tt>:from</tt>, a <tt>:to</tt> and a <tt>:klass</tt> attribute, from wchich the <tt>:klass</tt> attribute must be "String".
|
1033
|
+
# Internally all data's are stored as Strings (or array-of-strings if multi-valued). <tt>:from</tt> describes a Proc that will convert the
|
1034
|
+
# internally represented String to the class defined in <tt>:klass</tt> (which is currently a String), and <tt>:to</tt> will define the inverse of this conversion.
|
1035
|
+
# The whole <tt>:type</tt> attribute may be nil, which means there are no conversions, and the attribute is a String (or an Array of Strings).
|
1036
|
+
# The default value is that the <tt>:from</tt> and <tt>:to</tt> attributes are Proc objects that will return their parameter back. The <tt>:klass</tt> is always String,
|
1037
|
+
# and can not be changed. This type conversion will be done with all attribute changing methods, except #get_attribute, #set_attribute. Besides
|
1038
|
+
# the value of the <tt>:default_value</tt> parameter won't be converted either. Array_separator conversions are done before using this conversion.
|
1039
|
+
# Some types are defined as constants in PassiveLDAP::Types
|
1040
|
+
# * <tt>:multi_valued</tt>: tells whether the attribute can be multi_valued or not. multi_valued attributes will be arrays of string
|
1041
|
+
# * <tt>:level</tt>: sets the protection level that is needed to update this attribute. Check set_protection_level for details. Default is 0
|
1042
|
+
# * <tt>:name</tt>: sets the name/mapping of the attribute. By default it is the same as the attribute's name. When accessing the attribute
|
1043
|
+
# (using methods, [], get_variable, etc.) you have to reference it by it's new name. Internally the attributes will be stored with their
|
1044
|
+
# original attribute name.
|
1045
|
+
# * <tt>:default_value</tt>: the default value of the attribute, if the value of the attribute is empty when saving.
|
1046
|
+
# Must be a String/Array or a Proc object, that will return a String or an Array. The parameter of the proc object will
|
1047
|
+
# be the PassiveLDAP object itself. If nil there is no default value. Default is nil
|
1048
|
+
# * <tt>:hidden</tt>: if true, the object will be loaded from the directory, but it's not accessable using methods, [], and such, and
|
1049
|
+
# will be hidden from the columns too. The @attributes instance variable will still hold it's value, and it will be saved back to the directory
|
1050
|
+
# when changed. Useful for attributes like +objectclass+. Default is false.
|
1051
|
+
# * <tt>:always_update</tt>: if true, and there is a default value given, before save the attribute will always get it's default
|
1052
|
+
# value regardles of it's original value. Useful for timestamp or aggregate type attributes. Default is false.
|
1053
|
+
# * <tt>:read_only</tt>: sets the attribute to be read only. If a default value is given saving will update this attribute too if
|
1054
|
+
# it is empty. This is useful if the attribute needs a default value at creation but should be read-only otherwise. Default is false.
|
1055
|
+
#
|
1056
|
+
# TODO: more types
|
1057
|
+
#
|
1058
|
+
# TODO: name conflict checking for the mapped names
|
1059
|
+
#
|
1060
|
+
# Attributes must be lowercase symbols, because Net::LDAP treats them that way!
|
1061
|
+
#
|
1062
|
+
# example:
|
1063
|
+
# passive_ldap_attr :name => {}, :sn => {}, :cn => {}
|
1064
|
+
# passive_ldap_attr :name => {:level => 1}, :sn => {:level => 1}, :cn => {:level => 1}
|
1065
|
+
# passive_ldap_attr :mail => {:multi_valued => true, :level => 1}, :mobile => {:multi_valued => true, :level => 1}
|
1066
|
+
# passive_ldap_attr :roomnumber => {:level => 2}
|
1067
|
+
def passive_ldap_attr(attribs)
|
1068
|
+
mapto = {}
|
1069
|
+
mapfrom = {}
|
1070
|
+
nohidden = {}
|
1071
|
+
attribs.each { |key, value|
|
1072
|
+
value[:multi_valued] ||= false
|
1073
|
+
value[:level] ||= self.settings[:default_protection_level]
|
1074
|
+
value[:type] ||= nil
|
1075
|
+
if (value[:type]) then
|
1076
|
+
value[:type][:from] ||= Proc.new { |s| s }
|
1077
|
+
value[:type][:to] ||= Proc.new { |s| s }
|
1078
|
+
value[:type][:klass] = String
|
1079
|
+
end
|
1080
|
+
value[:name] ||= key
|
1081
|
+
value[:default_value] ||= nil
|
1082
|
+
value[:hidden] ||= false
|
1083
|
+
value[:always_update] ||= false
|
1084
|
+
value[:read_only] ||= false
|
1085
|
+
value[:read_only] = value[:read_only] or value[:hidden]
|
1086
|
+
raise DistinguishedNameException, "DN attribute can't have the always_update flag set" if key == :dn and value[:always_update]
|
1087
|
+
raise DistinguishedNameException, "DN attribute must be hidden" if key == :dn and !value[:hidden]
|
1088
|
+
raise DistinguishedNameException, "DN attribute must have a default_value" if key == :dn and value[:default_value].nil?
|
1089
|
+
unless value[:hidden]
|
1090
|
+
mapto[key] = value[:name]
|
1091
|
+
mapfrom[value[:name]] = key
|
1092
|
+
nohidden[key] = value
|
1093
|
+
end
|
1094
|
+
}
|
1095
|
+
write_inheritable_hash(:attr_orig, attribs)
|
1096
|
+
write_inheritable_hash(:attrs, nohidden)
|
1097
|
+
write_inheritable_hash(:mapto, mapto)
|
1098
|
+
write_inheritable_hash(:mapfrom, mapfrom)
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
# creates a new Net::LDAP object
|
1102
|
+
def initialize_ldap_con
|
1103
|
+
Net::LDAP.new( self.settings[:connection] )
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
# validates the format of each value in a multi-valued attribute. See ActiveRecord::Validations#validates_format_of.
|
1107
|
+
# Only use this with multi-valued attributes!
|
1108
|
+
def validates_format_of_each(*attr_names)
|
1109
|
+
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
|
1110
|
+
configuration.update(attr_names.extract_options!)
|
1111
|
+
|
1112
|
+
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
|
1113
|
+
validates_each(attr_names, configuration) do |record, attr_name, value|
|
1114
|
+
if value.nil? then
|
1115
|
+
record.errors.add(attr_name, configuration[:message])
|
1116
|
+
else
|
1117
|
+
if settings[:default_array_separator].nil? then
|
1118
|
+
value.each { |val|
|
1119
|
+
record.errors.add(attr_name, configuration[:message]) unless val.to_s =~ configuration[:with]
|
1120
|
+
}
|
1121
|
+
else
|
1122
|
+
value.split(settings[:default_array_separator]).each { |val|
|
1123
|
+
val.chomp!("\r") if settings[:default_array_separator] == "\n"
|
1124
|
+
record.errors.add(attr_name, configuration[:message]) unless val.to_s =~ configuration[:with]
|
1125
|
+
}
|
1126
|
+
end
|
1127
|
+
end
|
1128
|
+
end
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
=begin
|
1132
|
+
###########################################################
|
1133
|
+
# protected ActiveRecord compatible class methods
|
1134
|
+
###########################################################
|
1135
|
+
=end
|
1136
|
+
|
1137
|
+
# <b>AR</b> Defines an "attribute" method (like #inheritance_column or
|
1138
|
+
# #table_name). A new (class) method will be created with the
|
1139
|
+
# given name. If a value is specified, the new method will
|
1140
|
+
# return that value (as a string). Otherwise, the given block
|
1141
|
+
# will be used to compute the value of the method.
|
1142
|
+
#
|
1143
|
+
# The original method will be aliased, with the new name being
|
1144
|
+
# prefixed with "original_". This allows the new method to
|
1145
|
+
# access the original value.
|
1146
|
+
#
|
1147
|
+
# Example:
|
1148
|
+
#
|
1149
|
+
# class A < ActiveRecord::Base
|
1150
|
+
# define_attr_method :primary_key, "sysid"
|
1151
|
+
# define_attr_method( :inheritance_column ) do
|
1152
|
+
# original_inheritance_column + "_id"
|
1153
|
+
# end
|
1154
|
+
# end
|
1155
|
+
def define_attr_method(name, value=nil, &block)
|
1156
|
+
sing = class << self; self; end
|
1157
|
+
sing.send :alias_method, "original_#{name}", name
|
1158
|
+
if block_given?
|
1159
|
+
sing.send :define_method, name, &block
|
1160
|
+
else
|
1161
|
+
# use eval instead of a block to work around a memory leak in dev
|
1162
|
+
# mode in fcgi
|
1163
|
+
sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
=begin
|
1169
|
+
###########################################################
|
1170
|
+
# protected PassiveLDAP-only instance methods
|
1171
|
+
###########################################################
|
1172
|
+
=end
|
1173
|
+
|
1174
|
+
# creates a new Net::LDAP object and sets the username and pasword to the current protection level
|
1175
|
+
def initialize_ldap_con
|
1176
|
+
ldap = self.class.initialize_ldap_con
|
1177
|
+
ldap.authenticate(dn,@protection_password) if @protection_level == 1
|
1178
|
+
ldap.authenticate(@protection_username,@protection_password) if @protection_level >= 2
|
1179
|
+
ldap
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
# reads the attribute (using the name of the attribute as parameter)
|
1183
|
+
def read_mapped_attribute(attribute)
|
1184
|
+
att = attribute.kind_of?(Symbol) ? attribute : attribute.to_sym
|
1185
|
+
return self.id if att == :id
|
1186
|
+
v = get_attribute(att)
|
1187
|
+
raise AttributeAssignmentError, "Attribute #{att} does not exist" unless self.class.attr_mapfrom.has_key?(att)
|
1188
|
+
set = self.class.attrs[self.class.attr_mapfrom[att]][:type]
|
1189
|
+
if @array_separator and v.kind_of?(Array) then
|
1190
|
+
if set then
|
1191
|
+
v.collect { |v| set[:from].call(v) }.join(@array_separator)
|
1192
|
+
else
|
1193
|
+
v.join(@array_separator)
|
1194
|
+
end
|
1195
|
+
else
|
1196
|
+
if set then
|
1197
|
+
set[:from].call(v)
|
1198
|
+
else
|
1199
|
+
v
|
1200
|
+
end
|
1201
|
+
end
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
# writes the attribute (using the name of the attribute as parameter). Checks type (Array or not Array)
|
1205
|
+
def write_mapped_attribute(attribute,value)
|
1206
|
+
att = attribute.kind_of?(Symbol) ? attribute : attribute.to_sym
|
1207
|
+
if att == :id then
|
1208
|
+
self.id=value
|
1209
|
+
return value
|
1210
|
+
end
|
1211
|
+
multi_valued = false
|
1212
|
+
multi_valued = true if self.class.attr_mapfrom.has_key?(att) and self.class.attrs[self.class.attr_mapfrom[att]][:multi_valued]
|
1213
|
+
raise AttributeAssignmentError, "Attribute #{att} does not exist" unless self.class.attr_mapfrom.has_key?(att)
|
1214
|
+
set = self.class.attrs[self.class.attr_mapfrom[att]][:type]
|
1215
|
+
if @array_separator and multi_valued then
|
1216
|
+
val = value.split(@array_separator)
|
1217
|
+
val.each { |v|
|
1218
|
+
v.chomp!("\r") if @array_separator == "\n"
|
1219
|
+
v = set[:to].call(v) if set
|
1220
|
+
}
|
1221
|
+
set_attribute(att,val)
|
1222
|
+
else
|
1223
|
+
if multi_valued then
|
1224
|
+
value.each { |v|
|
1225
|
+
v = set[:to].call(v) if set
|
1226
|
+
}
|
1227
|
+
set_attribute(att,value)
|
1228
|
+
else
|
1229
|
+
set_attribute(att,set ? set[:to].call(value) : value)
|
1230
|
+
end
|
1231
|
+
end
|
1232
|
+
value
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
# calculates the mandatory attributes and stores them in the @attributes variable
|
1236
|
+
def calculate_mandatory_attributes
|
1237
|
+
self.class.attrs_all.each { |key, value|
|
1238
|
+
defval = value[:default_value]
|
1239
|
+
unless defval.nil?
|
1240
|
+
if @attributes.has_key?(key) and !@attributes[key].nil? and @attributes[key] != "" and @attributes[key] != [] then
|
1241
|
+
if value[:always_update] then
|
1242
|
+
if defval.respond_to?(:call) then
|
1243
|
+
@attributes[key] = defval.call(self)
|
1244
|
+
else
|
1245
|
+
@attributes[key] = defval
|
1246
|
+
end
|
1247
|
+
end
|
1248
|
+
else
|
1249
|
+
if defval.respond_to?(:call) then
|
1250
|
+
@attributes[key] = defval.call(self)
|
1251
|
+
else
|
1252
|
+
@attributes[key] = defval
|
1253
|
+
end
|
1254
|
+
end
|
1255
|
+
if self.class.attrs.has_key?(key) or key == :dn then
|
1256
|
+
alt_name = :dn
|
1257
|
+
alt_name = self.class.attrs[key][:name] unless key == :dn
|
1258
|
+
eval "@#{alt_name.to_s} = @attributes[key]"
|
1259
|
+
end
|
1260
|
+
end
|
1261
|
+
}
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
#######
|
1265
|
+
private
|
1266
|
+
#######
|
1267
|
+
|
1268
|
+
=begin
|
1269
|
+
###########################################################
|
1270
|
+
# private PassiveLDAP-only instance methods
|
1271
|
+
###########################################################
|
1272
|
+
=end
|
1273
|
+
|
1274
|
+
# change the password of a user an ActiveDirectory compatible way.
|
1275
|
+
#
|
1276
|
+
# The password in AD is stored in a write-only attribute called unicodePwd.
|
1277
|
+
# To set the password one need to supply a string encoded in UCS-2 Little Endian
|
1278
|
+
# which is surrounded by double quotes. The changing of the password is a bit tricky:
|
1279
|
+
#
|
1280
|
+
# * If the user wants to change his password he needs to delete the old password
|
1281
|
+
# and add the new password, both converted to the format described above.
|
1282
|
+
# * If a superuser wants to change someones password he needs to send a replace
|
1283
|
+
# command to the server.
|
1284
|
+
#
|
1285
|
+
# set_password_ad will convert the strings given to the correct format (using iconv)
|
1286
|
+
# then it will connect to the server (using the dn/password set with set_protection_level)
|
1287
|
+
# and finally will do the password change. Only the password will be sent to the server.
|
1288
|
+
#
|
1289
|
+
# the options hash has the following keys:
|
1290
|
+
# * <tt>:oldpass</tt>: the old password. If unset, the password specified with set_protection_level
|
1291
|
+
# will be used as the old password
|
1292
|
+
# * <tt>:superuser</tt>: if true, then the <tt>:oldpass</tt> attribute will be discarded, and
|
1293
|
+
# set_password will user the replace method to change the password. This would only work with
|
1294
|
+
# a superuser account
|
1295
|
+
# * <tt>:encoding</tt>: sets the encoding format of the source strings. Defaults to UTF-8
|
1296
|
+
#
|
1297
|
+
# Will raise RecordNotSaved with the result from the server if unsuccesful.
|
1298
|
+
#
|
1299
|
+
# Both newpass and oldpass may be a Proc object that would return a String. The block is called
|
1300
|
+
# with the record as parameter
|
1301
|
+
#
|
1302
|
+
# To change the password you need to use a secure (SSL with an at least 128-bit wide key) connection to the
|
1303
|
+
# server!
|
1304
|
+
def set_password_ad(newpass, options = nil) #:doc:
|
1305
|
+
options = {} if options.nil?
|
1306
|
+
options[:oldpass] ||= @protection_password
|
1307
|
+
options[:superuser] ||= false
|
1308
|
+
options[:encoding] ||= "UTF-8"
|
1309
|
+
|
1310
|
+
if newpass.respond_to?(:call) then
|
1311
|
+
np = Iconv.conv("UCS-2LE",options[:encoding],"\"#{newpass.call(self)}\"")
|
1312
|
+
else
|
1313
|
+
np = Iconv.conv("UCS-2LE",options[:encoding],"\"#{newpass}\"")
|
1314
|
+
end
|
1315
|
+
|
1316
|
+
ldap = initialize_ldap_con
|
1317
|
+
if options[:superuser] then
|
1318
|
+
ops = []
|
1319
|
+
ops << [:replace, :unicodepwd, np]
|
1320
|
+
ldap.modify :dn => dn, :operations => ops
|
1321
|
+
else
|
1322
|
+
if options[:oldpass].respond_to?(:call) then
|
1323
|
+
op = Iconv.conv("UCS-2LE",options[:encoding],"\"#{options[:oldpass].call(self)}\"")
|
1324
|
+
else
|
1325
|
+
op = Iconv.conv("UCS-2LE",options[:encoding],"\"#{options[:oldpass]}\"")
|
1326
|
+
end
|
1327
|
+
ops = []
|
1328
|
+
ops << [:delete, :unicodepwd, op]
|
1329
|
+
ops << [:add, :unicodepwd, np]
|
1330
|
+
ldap.modify :dn => dn, :operations => ops
|
1331
|
+
end
|
1332
|
+
raise RecordNotSaved, "LDAP error: #{ldap.get_operation_result.message}" unless ldap.get_operation_result.code == 0
|
1333
|
+
return true
|
1334
|
+
end
|
1335
|
+
|
1336
|
+
|
1337
|
+
=begin
|
1338
|
+
###########################################################
|
1339
|
+
# private ActiveRecord compatible instance methods
|
1340
|
+
###########################################################
|
1341
|
+
=end
|
1342
|
+
|
1343
|
+
# <b>AR</b> Initializes the attributes array with keys matching the columns from the linked table and
|
1344
|
+
# the values matching the corresponding default value of that column, so
|
1345
|
+
# that a new instance, or one populated from a passed-in Hash, still has all the attributes
|
1346
|
+
# that instances loaded from the database would.
|
1347
|
+
def attributes_from_column_definition
|
1348
|
+
self.class.columns.inject({}) do |attributes, column|
|
1349
|
+
attributes[column.name] = column.default unless column.name == self.class.primary_key
|
1350
|
+
attributes
|
1351
|
+
end
|
1352
|
+
end
|
1353
|
+
|
1354
|
+
# <b>ar</b>
|
1355
|
+
def create_or_update
|
1356
|
+
if new_record? then
|
1357
|
+
create
|
1358
|
+
else
|
1359
|
+
update
|
1360
|
+
end
|
1361
|
+
end
|
1362
|
+
|
1363
|
+
# <b>ar</b>
|
1364
|
+
def create
|
1365
|
+
calculate_mandatory_attributes
|
1366
|
+
raise RecordNotSaved, "distinguished name is missing" if @attributes[:dn].nil?
|
1367
|
+
ldap = initialize_ldap_con
|
1368
|
+
ops = {}
|
1369
|
+
@attributes.each { |key, value|
|
1370
|
+
if value.kind_of?(Integer) then value = value.to_s end
|
1371
|
+
if !value.nil? and value != "" and value != [] and key != :dn then
|
1372
|
+
if (self.class.attrs_all.has_key?(key) and self.class.attrs_all[key][:level] <= @protection_level) or
|
1373
|
+
(self.class.settings[:id_attribute] == key) then
|
1374
|
+
ops[key] = value
|
1375
|
+
end
|
1376
|
+
end
|
1377
|
+
}
|
1378
|
+
ldap.add :dn => dn, :attributes => ops
|
1379
|
+
raise RecordNotSaved, "ldap error: #{ldap.get_operation_result.message}" unless ldap.get_operation_result.code == 0
|
1380
|
+
@oldattr = @attributes.clone
|
1381
|
+
true
|
1382
|
+
end
|
1383
|
+
|
1384
|
+
# <b>ar</b>
|
1385
|
+
def update
|
1386
|
+
calculate_mandatory_attributes
|
1387
|
+
raise RecordNotSaved, "distinguished name is missing" if @attributes[:dn].nil?
|
1388
|
+
reload(:oldattr => true)
|
1389
|
+
addthis = {}
|
1390
|
+
deletethis = {}
|
1391
|
+
@attributes.each { |key, value|
|
1392
|
+
if !value.nil? and value != "" and value != [] then
|
1393
|
+
addthis[key] = value.duplicable? ? value.dup : value
|
1394
|
+
end
|
1395
|
+
}
|
1396
|
+
@oldattr.each { |key,value|
|
1397
|
+
if !value.nil? and value != "" and value != [] then
|
1398
|
+
if addthis.has_key?(key) then
|
1399
|
+
oval = value; oval = [oval] unless oval.kind_of?(Array)
|
1400
|
+
nval = addthis[key]; nval = [nval] unless nval.kind_of?(Array)
|
1401
|
+
oval.each { |val|
|
1402
|
+
if nval.include?(val) then
|
1403
|
+
# remove from the add list if the value existed when the record was loaded
|
1404
|
+
nval.delete(val)
|
1405
|
+
else
|
1406
|
+
# add to the delete list if the value doesn't exist
|
1407
|
+
deletethis[key] ||= []
|
1408
|
+
deletethis[key] << val
|
1409
|
+
end
|
1410
|
+
}
|
1411
|
+
if nval==[] then
|
1412
|
+
addthis.delete(key)
|
1413
|
+
else
|
1414
|
+
addthis[key] = nval
|
1415
|
+
end
|
1416
|
+
else
|
1417
|
+
# add to the delete list if the attribute doesn't exist
|
1418
|
+
val = value
|
1419
|
+
val = [val] unless val.kind_of?(Array)
|
1420
|
+
deletethis[key] = val
|
1421
|
+
end
|
1422
|
+
end
|
1423
|
+
}
|
1424
|
+
ldap = initialize_ldap_con
|
1425
|
+
ops = []
|
1426
|
+
deletethis.each { |key,value|
|
1427
|
+
if (self.class.attrs_all.has_key?(key) and self.class.attrs_all[key][:level] <= @protection_level) or
|
1428
|
+
(self.class.settings[:id_attribute] == key) then
|
1429
|
+
ops << [:delete, key, value]
|
1430
|
+
end if key != :dn
|
1431
|
+
}
|
1432
|
+
addthis.each { |key, value|
|
1433
|
+
if (self.class.attrs_all.has_key?(key) and self.class.attrs_all[key][:level] <= @protection_level) or
|
1434
|
+
(self.class.settings[:id_attribute] == key) then
|
1435
|
+
ops << [:add, key, value]
|
1436
|
+
end if key != :dn
|
1437
|
+
}
|
1438
|
+
if ops!=[] then
|
1439
|
+
ldap.modify :dn => dn, :operations => ops
|
1440
|
+
raise RecordNotSaved, "ldap error: #{ldap.get_operation_result.message}" unless ldap.get_operation_result.code == 0
|
1441
|
+
end
|
1442
|
+
@oldattr = @attributes.clone
|
1443
|
+
true
|
1444
|
+
end
|
1445
|
+
|
1446
|
+
|
1447
|
+
# default values
|
1448
|
+
passive_ldap :connection => {:host => "127.0.0.1", :port => "389", :auth => { :method => :anonymous } },
|
1449
|
+
:id_attribute => :id,
|
1450
|
+
:multiple_record_filter => Proc.new { |s| Net::LDAP::Filter.eq(s.settings[:id_attribute].id2name,"*") },
|
1451
|
+
:single_record_filter => Proc.new { |s,id| Net::LDAP::Filter.eq(s.settings[:id_attribute].id2name,id) },
|
1452
|
+
:record_base => "ou=users,dc=com",
|
1453
|
+
:record_scope => Net::LDAP::SearchScope_SingleLevel,
|
1454
|
+
:new_id => Proc.new { |s| 10000 + s.class.count*5 + rand(5) },
|
1455
|
+
:default_array_separator => nil,
|
1456
|
+
:default_protection_level => 0
|
1457
|
+
passive_ldap_attr({})
|
1458
|
+
|
1459
|
+
include ActiveRecord::Validations # some parts tested and they work
|
1460
|
+
include ActiveRecord::Locking::Optimistic # untested. likely to fail
|
1461
|
+
# include ActiveRecord::Locking::Pessimistic # sql specific
|
1462
|
+
include ActiveRecord::Callbacks # untested. likely to fail
|
1463
|
+
include ActiveRecord::Observing # untested. likely to fail
|
1464
|
+
include ActiveRecord::Timestamp # untested. likely to fail
|
1465
|
+
include ActiveRecord::Associations # untested. likely to fail
|
1466
|
+
include ActiveRecord::Aggregations # untested. likely to fail
|
1467
|
+
# include ActiveRecord::Transactions # sql specific
|
1468
|
+
include ActiveRecord::Reflection # untested. likely to fail. most of the reflection part is built-in
|
1469
|
+
# include ActiveRecord::Calculations # sql specific
|
1470
|
+
include ActiveRecord::Serialization # untested. likely to fail
|
1471
|
+
include ActiveRecord::AttributeMethods # untested. likely to fail
|
1472
|
+
end
|
1473
|
+
end
|