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