passiveldap 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|