passiveldap 0.1

Sign up to get free protection for your applications and to get access to all the features.
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