ucb_ldap 2.0.0.pre1 → 2.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/CHANGELOG +137 -135
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/{README → README.md} +82 -80
  7. data/Rakefile +38 -20
  8. data/lib/ucb_ldap.rb +238 -204
  9. data/lib/{ucb_ldap_address.rb → ucb_ldap/address.rb} +106 -106
  10. data/lib/{ucb_ldap_affiliation.rb → ucb_ldap/affiliation.rb} +16 -16
  11. data/lib/{ucb_ldap_entry.rb → ucb_ldap/entry.rb} +455 -448
  12. data/lib/{ucb_ldap_person_job_appointment.rb → ucb_ldap/job_appointment.rb} +77 -79
  13. data/lib/{ucb_ldap_namespace.rb → ucb_ldap/namespace.rb} +40 -50
  14. data/lib/{ucb_ldap_org.rb → ucb_ldap/org.rb} +427 -429
  15. data/lib/{ucb_ldap_person.rb → ucb_ldap/person.rb} +157 -148
  16. data/lib/{person → ucb_ldap/person}/affiliation_methods.rb +23 -22
  17. data/lib/ucb_ldap/person/common_attributes.rb +63 -0
  18. data/lib/{ucb_ldap_schema.rb → ucb_ldap/schema.rb} +28 -28
  19. data/lib/{ucb_ldap_schema_attribute.rb → ucb_ldap/schema_attribute.rb} +152 -153
  20. data/lib/{ucb_ldap_service.rb → ucb_ldap/service.rb} +17 -19
  21. data/lib/{ucb_ldap_student_term.rb → ucb_ldap/student_term.rb} +29 -31
  22. data/lib/ucb_ldap/version.rb +3 -0
  23. data/spec/rails_binds.yml +9 -0
  24. data/spec/spec_helper.rb +43 -0
  25. data/spec/ucb_ldap/address_spec.rb +54 -0
  26. data/spec/ucb_ldap/affiliation_spec.rb +85 -0
  27. data/spec/ucb_ldap/entry_spec.rb +241 -0
  28. data/spec/ucb_ldap/job_appointment_spec.rb +65 -0
  29. data/spec/ucb_ldap/namespace_spec.rb +72 -0
  30. data/spec/ucb_ldap/org_spec.rb +217 -0
  31. data/spec/ucb_ldap/person_spec.rb +225 -0
  32. data/spec/ucb_ldap/schema_attribute_spec.rb +122 -0
  33. data/spec/ucb_ldap/schema_spec.rb +104 -0
  34. data/spec/ucb_ldap/service_spec.rb +127 -0
  35. data/spec/ucb_ldap/student_term_spec.rb +121 -0
  36. data/spec/ucb_ldap_spec.rb +182 -0
  37. data/ucb_ldap.gemspec +20 -27
  38. metadata +113 -64
  39. data/Manifest +0 -23
  40. data/TODO +0 -2
  41. data/lib/person/adv_con_person.rb +0 -0
  42. data/lib/person/generic_attributes.rb +0 -68
  43. data/lib/ucb_ldap_exceptions.rb +0 -27
  44. data/version.yml +0 -1
@@ -1,106 +1,106 @@
1
-
2
- module UCB
3
- module LDAP
4
- # = UCB::LDAP::Address
5
- #
6
- # This class models a person address instance in the UCB LDAP directory.
7
- #
8
- # a = Address.find_by_uid("1234") #=> [#<UCB::LDAP::Address: ...>, ...]
9
- #
10
- # Addresses are usually loaded through a Person instance:
11
- #
12
- # p = Person.find_by_uid("1234") #=> #<UCB::LDAP::Person: ...>
13
- # addrs = p.addresses #=> [#<UCB::LDAP::Address: ...>, ...]
14
- #
15
- # == Note on Binds
16
- #
17
- # You must have a privileged bind and pass your credentials to UCB::LDAP.authenticate()
18
- # before performing your Address search.
19
- #
20
- class Address < Entry
21
- @entity_name = 'personAddress'
22
-
23
- def primary_work_address?
24
- berkeleyEduPersonAddressPrimaryFlag
25
- end
26
-
27
- def address_type
28
- berkeleyEduPersonAddressType
29
- end
30
-
31
- def building_code
32
- berkeleyEduPersonAddressBuildingCode
33
- end
34
-
35
- def city
36
- l.first
37
- end
38
-
39
- def country_code
40
- berkeleyEduPersonAddressCountryCode
41
- end
42
-
43
- def department_name
44
- berkeleyEduPersonAddressUnitCalNetDeptName
45
- end
46
-
47
- def department_acronym
48
- berkeleyEduPersonAddressUnitHRDeptName
49
- end
50
-
51
- def directories
52
- berkeleyEduPersonAddressPublications
53
- end
54
-
55
- # Returns email address associated with this Address.
56
- def email
57
- mail.first
58
- end
59
-
60
- def mail_code
61
- berkeleyEduPersonAddressMailCode
62
- end
63
-
64
- def mail_release?
65
- berkeleyEduEmailRelFlag
66
- end
67
-
68
- def phone
69
- telephoneNumber.first
70
- end
71
-
72
- # Returns postal address as an Array.
73
- #
74
- # addr.attribute(:postalAddress) #=> '501 Banway Bldg.$Berkeley, CA 94720-3814$USA'
75
- # addr.postal_address #=> ['501 Banway Bldg.', 'Berkeley, CA 94720-3814', 'USA']
76
- #
77
- def postal_address
78
- postalAddress == [] ? nil : postalAddress.split("$")
79
- end
80
-
81
- def sort_order
82
- berkeleyEduPersonAddressSortOrder.first || 0
83
- end
84
-
85
- def state
86
- st.first
87
- end
88
-
89
- def zip
90
- postalCode
91
- end
92
-
93
- class << self
94
- # Returns an Array of Address for <tt>uid</tt>, sorted by sort_order().
95
- # Returns an empty Array ([]) if nothing is found.
96
- #
97
- def find_by_uid(uid)
98
- base = "uid=#{uid},ou=people,dc=berkeley,dc=edu"
99
- filter = Net::LDAP::Filter.eq("objectclass", 'berkeleyEduPersonAddress')
100
- search(:base => base, :filter => filter).sort_by{|addr| addr.sort_order}
101
- end
102
-
103
- end
104
- end
105
- end
106
- end
1
+
2
+ module UCB
3
+ module LDAP
4
+ # = UCB::LDAP::Address
5
+ #
6
+ # This class models a person address instance in the UCB LDAP directory.
7
+ #
8
+ # a = Address.find_by_uid("1234") #=> [#<UCB::LDAP::Address: ...>, ...]
9
+ #
10
+ # Addresses are usually loaded through a Person instance:
11
+ #
12
+ # p = Person.find_by_uid("1234") #=> #<UCB::LDAP::Person: ...>
13
+ # addrs = p.addresses #=> [#<UCB::LDAP::Address: ...>, ...]
14
+ #
15
+ # == Note on Binds
16
+ #
17
+ # You must have a privileged bind and pass your credentials to UCB::LDAP.authenticate()
18
+ # before performing your Address search.
19
+ #
20
+ class Address < Entry
21
+ @entity_name = 'personAddress'
22
+
23
+ def primary_work_address?
24
+ berkeleyEduPersonAddressPrimaryFlag
25
+ end
26
+
27
+ def address_type
28
+ berkeleyEduPersonAddressType
29
+ end
30
+
31
+ def building_code
32
+ berkeleyEduPersonAddressBuildingCode
33
+ end
34
+
35
+ def city
36
+ l.first
37
+ end
38
+
39
+ def country_code
40
+ berkeleyEduPersonAddressCountryCode
41
+ end
42
+
43
+ def department_name
44
+ berkeleyEduPersonAddressUnitCalNetDeptName
45
+ end
46
+
47
+ def department_acronym
48
+ berkeleyEduPersonAddressUnitHRDeptName
49
+ end
50
+
51
+ def directories
52
+ berkeleyEduPersonAddressPublications
53
+ end
54
+
55
+ # Returns email address associated with this Address.
56
+ def email
57
+ mail.first
58
+ end
59
+
60
+ def mail_code
61
+ berkeleyEduPersonAddressMailCode
62
+ end
63
+
64
+ def mail_release?
65
+ berkeleyEduEmailRelFlag
66
+ end
67
+
68
+ def phone
69
+ telephoneNumber.first
70
+ end
71
+
72
+ # Returns postal address as an Array.
73
+ #
74
+ # addr.attribute(:postalAddress) #=> '501 Banway Bldg.$Berkeley, CA 94720-3814$USA'
75
+ # addr.postal_address #=> ['501 Banway Bldg.', 'Berkeley, CA 94720-3814', 'USA']
76
+ #
77
+ def postal_address
78
+ postalAddress == [] ? nil : postalAddress.split("$")
79
+ end
80
+
81
+ def sort_order
82
+ berkeleyEduPersonAddressSortOrder.first || 0
83
+ end
84
+
85
+ def state
86
+ st.first
87
+ end
88
+
89
+ def zip
90
+ postalCode
91
+ end
92
+
93
+ class << self
94
+ # Returns an Array of Address for <tt>uid</tt>, sorted by sort_order().
95
+ # Returns an empty Array ([]) if nothing is found.
96
+ #
97
+ def find_by_uid(uid)
98
+ base = "uid=#{uid},ou=people,dc=berkeley,dc=edu"
99
+ filter = Net::LDAP::Filter.eq("objectclass", 'berkeleyEduPersonAddress')
100
+ search(:base => base, :filter => filter).sort_by{|addr| addr.sort_order}
101
+ end
102
+
103
+ end
104
+ end
105
+ end
106
+ end
@@ -2,7 +2,7 @@
2
2
  module UCB
3
3
  module LDAP
4
4
  # = UCB::LDAP::Affiliation
5
- #
5
+ #
6
6
  # This class models a persons affiliate entries in the UCB LDAP directory.
7
7
  #
8
8
  # affiliations = Affiliation.find_by_uid("1234") #=> [#<UCB::LDAP::Affiliation: ...>, ...]
@@ -13,7 +13,7 @@ module UCB
13
13
  # affs = p.affiliations #=> [#<UCB::LDAP::Affiliation: ...>, ...]
14
14
  #
15
15
  # == Note on Binds
16
- #
16
+ #
17
17
  # You must have a privileged bind and pass your credentials to UCB::LDAP.authenticate()
18
18
  # before performing your Affiliation search.
19
19
  #
@@ -23,19 +23,19 @@ module UCB
23
23
  def create_datetime
24
24
  berkeleyEduAffCreateDate
25
25
  end
26
-
26
+
27
27
  def expired_by
28
28
  berkeleyEduAffExpBy
29
29
  end
30
-
30
+
31
31
  def expiration_date
32
32
  UCB::LDAP.local_date_parse(berkeleyEduAffExpDate)
33
33
  end
34
-
34
+
35
35
  def affiliate_id
36
36
  berkeleyEduAffID.first
37
37
  end
38
-
38
+
39
39
  def affiliate_type
40
40
  berkeleyEduAffType
41
41
  end
@@ -43,42 +43,42 @@ module UCB
43
43
  def first_name
44
44
  givenName.first
45
45
  end
46
-
46
+
47
47
  def middle_name
48
48
  berkeleyEduMiddleName
49
49
  end
50
-
50
+
51
51
  def last_name
52
52
  sn.first
53
53
  end
54
-
54
+
55
55
  def modified_by
56
56
  berkeleyEduModifiedBy
57
57
  end
58
-
58
+
59
59
  def source
60
60
  berkeleyEduPersonAffiliateSource
61
61
  end
62
-
62
+
63
63
  def dept_code
64
64
  departmentNumber.first
65
65
  end
66
-
66
+
67
67
  def dept_name
68
68
  berkeleyEduUnitCalNetDeptName
69
69
  end
70
-
70
+
71
71
  class << self
72
72
  # Returns an Array of Affiliation for <tt>uid</tt>.
73
- # Returns an empty Array ([]) if nothing is found.
73
+ # Returns an empty Array ([]) if nothing is found.
74
74
  #
75
75
  def find_by_uid(uid)
76
76
  base = "uid=#{uid},ou=people,dc=berkeley,dc=edu"
77
77
  filter = Net::LDAP::Filter.eq("objectclass", 'berkeleyEduPersonAffiliate')
78
78
  search(:base => base, :filter => filter)
79
79
  end
80
-
80
+
81
81
  end
82
82
  end
83
83
  end
84
- end
84
+ end
@@ -1,448 +1,455 @@
1
- module UCB
2
- module LDAP
3
- ##
4
- # = UCB::LDAP::Entry
5
- #
6
- # Abstract class representing an entry in the UCB LDAP directory. You
7
- # won't ever deal with Entry instances, but instead instances of Entry
8
- # sub-classes.
9
- #
10
- # == Accessing LDAP Attributes
11
- #
12
- # You will not see the attributes documented in the
13
- # instance method section of the documentation for Entry sub-classes,
14
- # even though you can access them as
15
- # if they _were_ instance methods.
16
- #
17
- # person = Person.find_by_uid("123") #=> #<UCB::LDAP::Person ..>
18
- # people.givenname #=> ["John"]
19
- #
20
- # Entry sub-classes may have convenience methods that
21
- # allow for accessing attributes by friendly names:
22
- #
23
- # person = Person.person_by_uid("123") #=> #<UCB::LDAP::Person ..>
24
- # person.firstname #=> "John"
25
- #
26
- # See the sub-class documentation for specifics.
27
- #
28
- # ===Single- / Multi-Value Attributes
29
- #
30
- # Attribute values are returned as arrays or scalars based on how
31
- # they are defined in the LDAP schema.
32
- #
33
- # Entry subclasses may have convenience
34
- # methods that return scalars even though the schema defines
35
- # the unerlying attribute as multi-valued becuase in practice the are single-valued.
36
- #
37
- # === Attribute Types
38
- #
39
- # Attribute values are stored as arrays of strings in LDAP, but
40
- # when accessed through Entry sub-class methods are returned
41
- # cast to their Ruby type as defined in the schema. Types are one of:
42
- #
43
- # * string
44
- # * integer
45
- # * boolean
46
- # * datetime
47
- #
48
- # === Missing Attribute Values
49
- #
50
- # If an attribute value is not present, the value returned depends on
51
- # type and multi/single value field:
52
- #
53
- # * empty multi-valued attributes return an empty array ([])
54
- # * empty booleans return +false+
55
- # * everything else returns +nil+ if empty
56
- #
57
- # Attempting to get or set an attribute value for an invalid attriubte name
58
- # will raise a BadAttributeNameException.
59
- #
60
- # == Updating LDAP
61
- #
62
- # If your bind has privleges for updating the directory you can update
63
- # the directory using methods of Entry sub-classes. Make sure you call
64
- # UCB::LDAP.authenticate before calling any update methods.
65
- #
66
- # There are three pairs of update methods that behave like Rails ActiveRecord
67
- # methods of the same name. These methods are fairly thin wrappers around
68
- # standard LDAP update commands.
69
- #
70
- # The "bang" methods (those ending in "!") differ from their bangless
71
- # counterparts in that the bang methods raise +DirectoryNotUpdatedException+
72
- # on failure, while the bangless return +false+.
73
- #
74
- # * #create/#create! - class methods that do LDAP add
75
- # * #update_attributes/#update_attributes! - instance methods that do LDAP modify
76
- # * #delete/#delete! - instance methods that do LDAP delete
77
- #
78
- class Entry
79
-
80
- ##
81
- # Returns new instance of UCB::LDAP::Entry. The argument
82
- # net_ldap_entry is an instance of Net::LDAP::Entry.
83
- #
84
- # You should not need to create any UCB::LDAP::Entry instances;
85
- # they are created by calls to UCB::LDAP.search and friends.
86
- #
87
- def initialize(net_ldap_entry) #:nodoc:
88
- # Don't store Net::LDAP entry in object since it uses the block
89
- # initialization method of Hash which can't be marshalled ... this
90
- # means it can't be stored in a Rails session.
91
- @attributes = {}
92
- net_ldap_entry.each do |attr, value|
93
- @attributes[canonical(attr)] = value.map{|v| v.dup}
94
- end
95
- end
96
-
97
- ##
98
- # <tt>Hash</tt> of attributes returned from underlying NET::LDAP::Entry
99
- # instance. Hash keys are #canonical attribute names, hash values are attribute
100
- # values <em>as returned from LDAP</em>, i.e. arrays.
101
- #
102
- # You should most likely be referencing attributes as if they were
103
- # instance methods rather than directly through this method. See top of
104
- # this document.
105
- #
106
- def attributes
107
- @attributes
108
- end
109
-
110
- ##
111
- # Returns the value of the <em>Distinguished Name</em> attribute.
112
- #
113
- def dn
114
- attributes[canonical(:dn)]
115
- end
116
-
117
- def canonical(string_or_symbol) #:nodoc:
118
- self.class.canonical(string_or_symbol)
119
- end
120
-
121
- ##
122
- # Update an existing entry. Returns entry if successful else false.
123
- #
124
- # attrs = {:attr1 => "new_v1", :attr2 => "new_v2"}
125
- # entry.update_attributes(attrs)
126
- #
127
- def update_attributes(attrs)
128
- attrs.each{|k, v| self.send("#{k}=", v)}
129
- if modify()
130
- @attributes = self.class.find_by_dn(dn).attributes.dup
131
- return true
132
- end
133
- false
134
- end
135
-
136
- ##
137
- # Same as #update_attributes(), but raises DirectoryNotUpdated on failure.
138
- #
139
- def update_attributes!(attrs)
140
- update_attributes(attrs) || raise(DirectoryNotUpdatedException)
141
- end
142
-
143
- ##
144
- # Delete entry. Returns +true+ on sucess, +false+ on failure.
145
- #
146
- def delete
147
- net_ldap.delete(:dn => dn)
148
- end
149
-
150
- ##
151
- # Same as #delete() except raises DirectoryNotUpdated on failure.
152
- #
153
- def delete!
154
- delete || raise(DirectoryNotUpdatedException)
155
- end
156
-
157
- def net_ldap
158
- self.class.net_ldap
159
- end
160
-
161
-
162
- private unless $TESTING
163
-
164
- ##
165
- # Used to get/set attribute values.
166
- #
167
- # If we can't make an attribute name out of method, let
168
- # regular method_missing() handle it.
169
- #
170
- def method_missing(method, *args) #:nodoc:
171
- setter_method?(method) ? value_setter(method, *args) : value_getter(method)
172
- rescue BadAttributeNameException
173
- return super
174
- end
175
-
176
- ##
177
- # Returns +true+ if _method_ is a "setter", i.e., ends in "=".
178
- #
179
- def setter_method?(method)
180
- method.to_s[-1, 1] == "="
181
- end
182
-
183
- ##
184
- # Called by method_missing() to get an attribute value.
185
- #
186
- def value_getter(method)
187
- schema_attribute = self.class.schema_attribute(method)
188
- raw_value = attributes[canonical(schema_attribute.name)]
189
- schema_attribute.get_value(raw_value)
190
- end
191
-
192
- ##
193
- # Called by method_missing() to set an attribute value.
194
- #
195
- def value_setter(method, *args)
196
- schema_attribute = self.class.schema_attribute(method.to_s.chop)
197
- attr_key = canonical(schema_attribute.name)
198
- assigned_attributes[attr_key] = schema_attribute.ldap_value(args[0])
199
- end
200
-
201
- def assigned_attributes
202
- @assigned_attributes ||= {}
203
- end
204
-
205
- def modify_operations
206
- ops = []
207
- assigned_attributes.keys.sort_by{|k| k.to_s}.each do |key|
208
- value = assigned_attributes[key]
209
- op = value.nil? ? :delete : :replace
210
- ops << [op, key, value]
211
- end
212
- ops
213
- end
214
-
215
- def modify()
216
- if UCB::LDAP.net_ldap.modify(:dn => dn, :operations => modify_operations)
217
- @assigned_attributes = nil
218
- return true
219
- end
220
- false
221
- end
222
-
223
- # Class methods
224
- class << self
225
-
226
- public
227
-
228
- # Creates and returns new entry. Returns +false+ if unsuccessful.
229
- # Sets :objectclass key of <em>args[:attributes]</em> to
230
- # object_classes read from schema.
231
- #
232
- # dn = "uid=999999,ou=people,dc=example,dc=com"
233
- # attr = {
234
- # :uid => "999999",
235
- # :mail => "gsmith@example.com"
236
- # }
237
- #
238
- # EntrySubClass.create(:dn => dn, :attributes => attr) #=> #<UCB::LDAP::EntrySubClass ..>
239
- #
240
- # Caller is responsible for setting :dn and :attributes correctly,
241
- # as well as any other validation.
242
- #
243
- def create(args)
244
- args[:attributes][:objectclass] = object_classes
245
- result = net_ldap.add(args)
246
- result or return false
247
- find_by_dn(args[:dn])
248
- end
249
-
250
- ##
251
- # Returns entry whose distinguised name is _dn_.
252
- def find_by_dn(dn)
253
- search(
254
- :base => dn,
255
- :scope => Net::LDAP::SearchScope_BaseObject,
256
- :filter => "objectClass=*"
257
- ).first
258
- end
259
-
260
- ##
261
- # Same as #create(), but raises DirectoryNotUpdated on failure.
262
- def create!(args)
263
- create(args) || raise(DirectoryNotUpdatedException)
264
- end
265
-
266
- ##
267
- # Returns a new Net::LDAP::Filter that is the result of combining
268
- # <em>filters</em> using <em>operator</em> (<em>filters</em> is
269
- # an +Array+ of Net::LDAP::Filter).
270
- #
271
- # See Net::LDAP#& and Net::LDAP#| for details.
272
- #
273
- # f1 = Net::LDAP::Filter.eq("lastname", "hansen")
274
- # f2 = Net::LDAP::Filter.eq("firstname", "steven")
275
- #
276
- # combine_filters([f1, f2]) # same as: f1 & f2
277
- # combine_filters([f1, f2], '|') # same as: f1 | f2
278
- #
279
- def combine_filters(filters, operator = '&')
280
- filters.inject{|accum, filter| accum.send(operator, filter)}
281
- end
282
-
283
- ##
284
- # Returns Net::LDAP::Filter. Allows for <em>filter</em> to
285
- # be a +Hash+ of :key => value. Filters are combined with "&".
286
- #
287
- # UCB::LDAP::Entry.make_search_filter(:uid => '123')
288
- # UCB::LDAP::Entry.make_search_filter(:a1 => v1, :a2 => v2)
289
- #
290
- def make_search_filter(filter)
291
- return filter if filter.instance_of? Net::LDAP::Filter
292
- return filter if filter.instance_of? String
293
-
294
- filters = []
295
- # sort so result is predictable for unit test
296
- filter.keys.sort_by { |symbol| "#{symbol}" }.each do |attr|
297
- filters << Net::LDAP::Filter.eq("#{attr}", "#{filter[attr]}")
298
- end
299
- combine_filters(filters, "&")
300
- end
301
-
302
- ##
303
- # Returns +Array+ of object classes making up this type of LDAP entity.
304
- def object_classes
305
- @object_classes ||= UCB::LDAP::Schema.schema_hash[entity_name]["objectClasses"]
306
- end
307
-
308
- def unique_object_class
309
- @unique_object_class ||= UCB::LDAP::Schema.schema_hash[entity_name]["uniqueObjectClass"]
310
- end
311
-
312
- ##
313
- # returns an Array of symbols where each symbol is the name of
314
- # a required attribute for the Entry
315
- def required_attributes
316
- required_schema_attributes.keys
317
- end
318
-
319
- ##
320
- # returns Hash of SchemaAttribute objects that are required
321
- # for the Entry. Each SchemaAttribute object is keyed to the
322
- # attribute's name.
323
- #
324
- # Note: required_schema_attributes will not return aliases, it
325
- # only returns the original attributes
326
- #
327
- # Example:
328
- # Person.required_schema_attribues[:cn]
329
- # => <UCB::LDAP::Schema::Attribute:0x11c6b68>
330
- #
331
- def required_schema_attributes
332
- required_atts = schema_attributes_hash.reject { |key, value| !value.required? }
333
- required_atts.reject do |key, value|
334
- aliases = value.aliases.map { |a| canonical(a) }
335
- aliases.include?(key)
336
- end
337
- end
338
-
339
- ##
340
- # Returns an +Array+ of Schema::Attribute for the entity.
341
- #
342
- def schema_attributes_array
343
- @schema_attributes_array || set_schema_attributes
344
- @schema_attributes_array
345
- end
346
-
347
- ##
348
- # Returns as +Hash+ whose keys are the canonical attribute names
349
- # and whose values are the corresponding Schema::Attributes.
350
- #
351
- def schema_attributes_hash
352
- @schema_attributes_hash || set_schema_attributes
353
- @schema_attributes_hash
354
- end
355
-
356
- def schema_attribute(attribute_name)
357
- schema_attributes_hash[canonical(attribute_name)] ||
358
- raise(BadAttributeNameException, "'#{attribute_name}' is not a recognized attribute name")
359
- end
360
-
361
- ##
362
- # Returns Array of UCB::LDAP::Entry for entries matching _args_.
363
- # When called from a subclass, returns Array of subclass instances.
364
- #
365
- # See Net::LDAP::search for more information on _args_.
366
- #
367
- # Most common arguments are <tt>:base</tt> and <tt>:filter</tt>.
368
- # Search methods of subclasses have default <tt>:base</tt> that
369
- # can be overriden.
370
- #
371
- # See make_search_filter for <tt>:filter</tt> options.
372
- #
373
- # base = "ou=people,dc=berkeley,dc=edu"
374
- # entries = UCB::LDAP::Entry.search(:base => base, :filter => {:uid => '123'})
375
- # entries = UCB::LDAP::Entry.search(:base => base, :filter => {:sn => 'Doe', :givenname => 'John'}
376
- #
377
- def search(args={})
378
- args = args.dup
379
- args[:base] ||= tree_base
380
- args[:filter] = make_search_filter args[:filter] if args[:filter]
381
-
382
- results = []
383
- net_ldap.search(args) do |entry|
384
- results << new(entry)
385
- end
386
- results
387
- end
388
-
389
- ##
390
- # Returns the canonical representation of a symbol or string so
391
- # we can look up attributes in a number of ways.
392
- #
393
- def canonical(string_or_symbol)
394
- string_or_symbol.to_s.downcase.to_sym
395
- end
396
-
397
- ##
398
- # Returns underlying Net::LDAP instance.
399
- #
400
- def net_ldap #:nodoc:
401
- UCB::LDAP.net_ldap
402
- end
403
-
404
- private unless $TESTING
405
-
406
- ##
407
- # Schema entity name. Set in each subclass.
408
- #
409
- def entity_name
410
- @entity_name
411
- end
412
-
413
- ##
414
- # Want an array of Schema::Attributes as well as a hash
415
- # of all possible variations on a name pointing to correct array element.
416
- #
417
- def set_schema_attributes
418
- @schema_attributes_array = []
419
- @schema_attributes_hash = {}
420
- UCB::LDAP::Schema.schema_hash[entity_name]["attributes"].each do |k, v|
421
- sa = UCB::LDAP::Schema::Attribute.new(v.merge("name" => k))
422
- @schema_attributes_array << sa
423
- [sa.name, sa.aliases].flatten.each do |name|
424
- @schema_attributes_hash[canonical(name)] = sa
425
- end
426
- end
427
- rescue
428
- raise "Error loading schema attributes for entity_name '#{entity_name}'"
429
- end
430
-
431
- ##
432
- # Returns tree base for LDAP searches. Subclasses each have
433
- # their own value.
434
- #
435
- # Can be overridden in #search by passing in a <tt>:base</tt> parm.
436
- ##
437
- def tree_base
438
- @tree_base
439
- end
440
-
441
- def tree_base=(tree_base)
442
- @tree_base = tree_base
443
- end
444
-
445
- end # end of class methods
446
- end
447
- end
448
- end
1
+ module UCB
2
+ module LDAP
3
+ ##
4
+ # = UCB::LDAP::Entry
5
+ #
6
+ # Abstract class representing an entry in the UCB LDAP directory. You
7
+ # won't ever deal with Entry instances, but instead instances of Entry
8
+ # sub-classes.
9
+ #
10
+ # == Accessing LDAP Attributes
11
+ #
12
+ # You will not see the attributes documented in the
13
+ # instance method section of the documentation for Entry sub-classes,
14
+ # even though you can access them as
15
+ # if they _were_ instance methods.
16
+ #
17
+ # person = Person.find_by_uid("123") #=> #<UCB::LDAP::Person ..>
18
+ # people.givenname #=> ["John"]
19
+ #
20
+ # Entry sub-classes may have convenience methods that
21
+ # allow for accessing attributes by friendly names:
22
+ #
23
+ # person = Person.person_by_uid("123") #=> #<UCB::LDAP::Person ..>
24
+ # person.firstname #=> "John"
25
+ #
26
+ # See the sub-class documentation for specifics.
27
+ #
28
+ # ===Single- / Multi-Value Attributes
29
+ #
30
+ # Attribute values are returned as arrays or scalars based on how
31
+ # they are defined in the LDAP schema.
32
+ #
33
+ # Entry subclasses may have convenience
34
+ # methods that return scalars even though the schema defines
35
+ # the unerlying attribute as multi-valued becuase in practice the are single-valued.
36
+ #
37
+ # === Attribute Types
38
+ #
39
+ # Attribute values are stored as arrays of strings in LDAP, but
40
+ # when accessed through Entry sub-class methods are returned
41
+ # cast to their Ruby type as defined in the schema. Types are one of:
42
+ #
43
+ # * string
44
+ # * integer
45
+ # * boolean
46
+ # * datetime
47
+ #
48
+ # === Missing Attribute Values
49
+ #
50
+ # If an attribute value is not present, the value returned depends on
51
+ # type and multi/single value field:
52
+ #
53
+ # * empty multi-valued attributes return an empty array ([])
54
+ # * empty booleans return +false+
55
+ # * everything else returns +nil+ if empty
56
+ #
57
+ # Attempting to get or set an attribute value for an invalid attriubte name
58
+ # will raise a BadAttributeNameException.
59
+ #
60
+ # == Updating LDAP
61
+ #
62
+ # If your bind has privleges for updating the directory you can update
63
+ # the directory using methods of Entry sub-classes. Make sure you call
64
+ # UCB::LDAP.authenticate before calling any update methods.
65
+ #
66
+ # There are three pairs of update methods that behave like Rails ActiveRecord
67
+ # methods of the same name. These methods are fairly thin wrappers around
68
+ # standard LDAP update commands.
69
+ #
70
+ # The "bang" methods (those ending in "!") differ from their bangless
71
+ # counterparts in that the bang methods raise +DirectoryNotUpdatedException+
72
+ # on failure, while the bangless return +false+.
73
+ #
74
+ # * #create/#create! - class methods that do LDAP add
75
+ # * #update_attributes/#update_attributes! - instance methods that do LDAP modify
76
+ # * #delete/#delete! - instance methods that do LDAP delete
77
+ #
78
+ class Entry
79
+ TESTING = false
80
+
81
+ ##
82
+ # Returns new instance of UCB::LDAP::Entry. The argument
83
+ # net_ldap_entry is an instance of Net::LDAP::Entry.
84
+ #
85
+ # You should not need to create any UCB::LDAP::Entry instances;
86
+ # they are created by calls to UCB::LDAP.search and friends.
87
+ #
88
+ def initialize(net_ldap_entry) #:nodoc:
89
+ # Don't store Net::LDAP entry in object since it uses the block
90
+ # initialization method of Hash which can't be marshalled ... this
91
+ # means it can't be stored in a Rails session.
92
+ @attributes = {}
93
+ net_ldap_entry.each do |attr, value|
94
+ @attributes[canonical(attr)] = value.map { |v| v.dup }
95
+ end
96
+ end
97
+
98
+ ##
99
+ # <tt>Hash</tt> of attributes returned from underlying NET::LDAP::Entry
100
+ # instance. Hash keys are #canonical attribute names, hash values are attribute
101
+ # values <em>as returned from LDAP</em>, i.e. arrays.
102
+ #
103
+ # You should most likely be referencing attributes as if they were
104
+ # instance methods rather than directly through this method. See top of
105
+ # this document.
106
+ #
107
+ def attributes
108
+ @attributes
109
+ end
110
+
111
+ ##
112
+ # Returns the value of the <em>Distinguished Name</em> attribute.
113
+ #
114
+ def dn
115
+ attributes[canonical(:dn)]
116
+ end
117
+
118
+ def canonical(string_or_symbol) #:nodoc:
119
+ self.class.canonical(string_or_symbol)
120
+ end
121
+
122
+ ##
123
+ # Update an existing entry. Returns entry if successful else false.
124
+ #
125
+ # attrs = {:attr1 => "new_v1", :attr2 => "new_v2"}
126
+ # entry.update_attributes(attrs)
127
+ #
128
+ def update_attributes(attrs)
129
+ attrs.each { |k, v| self.send("#{k}=", v) }
130
+ if modify
131
+ @attributes = self.class.find_by_dn(dn).attributes.dup
132
+ return true
133
+ end
134
+ false
135
+ end
136
+
137
+ ##
138
+ # Same as #update_attributes(), but raises DirectoryNotUpdated on failure.
139
+ #
140
+ def update_attributes!(attrs)
141
+ update_attributes(attrs) || raise(DirectoryNotUpdatedException)
142
+ end
143
+
144
+ ##
145
+ # Delete entry. Returns +true+ on sucess, +false+ on failure.
146
+ #
147
+ def delete
148
+ net_ldap.delete(:dn => dn)
149
+ end
150
+
151
+ ##
152
+ # Same as #delete() except raises DirectoryNotUpdated on failure.
153
+ #
154
+ def delete!
155
+ delete || raise(DirectoryNotUpdatedException)
156
+ end
157
+
158
+ def net_ldap
159
+ self.class.net_ldap
160
+ end
161
+
162
+
163
+ #private unless TESTING
164
+
165
+ ##
166
+ # Used to get/set attribute values.
167
+ #
168
+ # If we can't make an attribute name out of method, let
169
+ # regular method_missing() handle it.
170
+ #
171
+ def method_missing(method, *args) #:nodoc:
172
+ setter_method?(method) ? value_setter(method, *args) : value_getter(method)
173
+ rescue BadAttributeNameException
174
+ return super
175
+ end
176
+
177
+ ##
178
+ # Returns +true+ if _method_ is a "setter", i.e., ends in "=".
179
+ #
180
+ def setter_method?(method)
181
+ method.to_s[-1, 1] == "="
182
+ end
183
+
184
+ ##
185
+ # Called by method_missing() to get an attribute value.
186
+ #
187
+ def value_getter(method)
188
+ schema_attribute = self.class.schema_attribute(method)
189
+ raw_value = attributes[canonical(schema_attribute.name)]
190
+ schema_attribute.get_value(raw_value)
191
+ end
192
+
193
+ ##
194
+ # Called by method_missing() to set an attribute value.
195
+ #
196
+ def value_setter(method, *args)
197
+ schema_attribute = self.class.schema_attribute(method.to_s.chop)
198
+ attr_key = canonical(schema_attribute.name)
199
+ assigned_attributes[attr_key] = schema_attribute.ldap_value(args[0])
200
+ end
201
+
202
+ def assigned_attributes
203
+ @assigned_attributes ||= {}
204
+ end
205
+
206
+ def modify_operations
207
+ ops = []
208
+ assigned_attributes.keys.sort_by { |k| k.to_s }.each do |key|
209
+ value = assigned_attributes[key]
210
+ op = value.nil? ? :delete : :replace
211
+ ops << [op, key, value]
212
+ end
213
+ ops
214
+ end
215
+
216
+ def modify()
217
+ if UCB::LDAP.net_ldap.modify(:dn => dn, :operations => modify_operations)
218
+ @assigned_attributes = nil
219
+ return true
220
+ end
221
+ false
222
+ end
223
+
224
+ # Class methods
225
+ class << self
226
+
227
+ public
228
+
229
+ def filter_in(attribute_name, array_of_values)
230
+ filters = array_of_values.map { |value| Net::LDAP::Filter.eq(attribute_name, value) }
231
+ UCB::LDAP::Entry.combine_filters(filters, '|')
232
+ end
233
+
234
+ # Creates and returns new entry. Returns +false+ if unsuccessful.
235
+ # Sets :objectclass key of <em>args[:attributes]</em> to
236
+ # object_classes read from schema.
237
+ #
238
+ # dn = "uid=999999,ou=people,dc=example,dc=com"
239
+ # attr = {
240
+ # :uid => "999999",
241
+ # :mail => "gsmith@example.com"
242
+ # }
243
+ #
244
+ # EntrySubClass.create(:dn => dn, :attributes => attr) #=> #<UCB::LDAP::EntrySubClass ..>
245
+ #
246
+ # Caller is responsible for setting :dn and :attributes correctly,
247
+ # as well as any other validation.
248
+ #
249
+ def create(args)
250
+ args[:attributes][:objectclass] = object_classes
251
+ result = net_ldap.add(args)
252
+ result or return false
253
+ find_by_dn(args[:dn])
254
+ end
255
+
256
+ ##
257
+ # Returns entry whose distinguised name is _dn_.
258
+ def find_by_dn(dn)
259
+ search(
260
+ :base => dn,
261
+ :scope => Net::LDAP::SearchScope_BaseObject,
262
+ :filter => "objectClass=*"
263
+ ).first
264
+ end
265
+
266
+ ##
267
+ # Same as #create(), but raises DirectoryNotUpdated on failure.
268
+ def create!(args)
269
+ create(args) || raise(DirectoryNotUpdatedException)
270
+ end
271
+
272
+ ##
273
+ # Returns a new Net::LDAP::Filter that is the result of combining
274
+ # <em>filters</em> using <em>operator</em> (<em>filters</em> is
275
+ # an +Array+ of Net::LDAP::Filter).
276
+ #
277
+ # See Net::LDAP#& and Net::LDAP#| for details.
278
+ #
279
+ # f1 = Net::LDAP::Filter.eq("lastname", "hansen")
280
+ # f2 = Net::LDAP::Filter.eq("firstname", "steven")
281
+ #
282
+ # combine_filters([f1, f2]) # same as: f1 & f2
283
+ # combine_filters([f1, f2], '|') # same as: f1 | f2
284
+ #
285
+ def combine_filters(filters, operator = '&')
286
+ filters.inject { |accum, filter| accum.send(operator, filter) }
287
+ end
288
+
289
+ ##
290
+ # Returns Net::LDAP::Filter. Allows for <em>filter</em> to
291
+ # be a +Hash+ of :key => value. Filters are combined with "&".
292
+ #
293
+ # UCB::LDAP::Entry.make_search_filter(:uid => '123')
294
+ # UCB::LDAP::Entry.make_search_filter(:a1 => v1, :a2 => v2)
295
+ #
296
+ def make_search_filter(filter)
297
+ return filter if filter.instance_of? Net::LDAP::Filter
298
+ return filter if filter.instance_of? String
299
+
300
+ filters = []
301
+ # sort so result is predictable for unit test
302
+ filter.keys.sort_by { |symbol| "#{symbol}" }.each do |attr|
303
+ filters << Net::LDAP::Filter.eq("#{attr}", "#{filter[attr]}")
304
+ end
305
+ combine_filters(filters, "&")
306
+ end
307
+
308
+ ##
309
+ # Returns +Array+ of object classes making up this type of LDAP entity.
310
+ def object_classes
311
+ @object_classes ||= UCB::LDAP::Schema.schema_hash[entity_name]["objectClasses"]
312
+ end
313
+
314
+ def unique_object_class
315
+ @unique_object_class ||= UCB::LDAP::Schema.schema_hash[entity_name]["uniqueObjectClass"]
316
+ end
317
+
318
+ ##
319
+ # returns an Array of symbols where each symbol is the name of
320
+ # a required attribute for the Entry
321
+ def required_attributes
322
+ required_schema_attributes.keys
323
+ end
324
+
325
+ ##
326
+ # returns Hash of SchemaAttribute objects that are required
327
+ # for the Entry. Each SchemaAttribute object is keyed to the
328
+ # attribute's name.
329
+ #
330
+ # Note: required_schema_attributes will not return aliases, it
331
+ # only returns the original attributes
332
+ #
333
+ # Example:
334
+ # Person.required_schema_attribues[:cn]
335
+ # => <UCB::LDAP::Schema::Attribute:0x11c6b68>
336
+ #
337
+ def required_schema_attributes
338
+ required_atts = schema_attributes_hash.reject { |key, value| !value.required? }
339
+ required_atts.reject do |key, value|
340
+ aliases = value.aliases.map { |a| canonical(a) }
341
+ aliases.include?(key)
342
+ end
343
+ end
344
+
345
+ ##
346
+ # Returns an +Array+ of Schema::Attribute for the entity.
347
+ #
348
+ def schema_attributes_array
349
+ @schema_attributes_array || set_schema_attributes
350
+ @schema_attributes_array
351
+ end
352
+
353
+ ##
354
+ # Returns as +Hash+ whose keys are the canonical attribute names
355
+ # and whose values are the corresponding Schema::Attributes.
356
+ #
357
+ def schema_attributes_hash
358
+ @schema_attributes_hash || set_schema_attributes
359
+ @schema_attributes_hash
360
+ end
361
+
362
+ def schema_attribute(attribute_name)
363
+ schema_attributes_hash[canonical(attribute_name)] ||
364
+ raise(BadAttributeNameException, "'#{attribute_name}' is not a recognized attribute name")
365
+ end
366
+
367
+ ##
368
+ # Returns Array of UCB::LDAP::Entry for entries matching _args_.
369
+ # When called from a subclass, returns Array of subclass instances.
370
+ #
371
+ # See Net::LDAP::search for more information on _args_.
372
+ #
373
+ # Most common arguments are <tt>:base</tt> and <tt>:filter</tt>.
374
+ # Search methods of subclasses have default <tt>:base</tt> that
375
+ # can be overriden.
376
+ #
377
+ # See make_search_filter for <tt>:filter</tt> options.
378
+ #
379
+ # base = "ou=people,dc=berkeley,dc=edu"
380
+ # entries = UCB::LDAP::Entry.search(:base => base, :filter => {:uid => '123'})
381
+ # entries = UCB::LDAP::Entry.search(:base => base, :filter => {:sn => 'Doe', :givenname => 'John'}
382
+ #
383
+ def search(args={})
384
+ args = args.dup
385
+ args[:base] ||= tree_base
386
+ args[:filter] = make_search_filter args[:filter] if args[:filter]
387
+
388
+ results = []
389
+ net_ldap.search(args) do |entry|
390
+ results << new(entry)
391
+ end
392
+ results
393
+ end
394
+
395
+ ##
396
+ # Returns the canonical representation of a symbol or string so
397
+ # we can look up attributes in a number of ways.
398
+ #
399
+ def canonical(string_or_symbol)
400
+ string_or_symbol.to_s.downcase.to_sym
401
+ end
402
+
403
+ ##
404
+ # Returns underlying Net::LDAP instance.
405
+ #
406
+ def net_ldap #:nodoc:
407
+ UCB::LDAP.net_ldap
408
+ end
409
+
410
+ ## TODO: restore private
411
+ # private unless $TESTING
412
+
413
+ ##
414
+ # Schema entity name. Set in each subclass.
415
+ #
416
+ def entity_name
417
+ @entity_name
418
+ end
419
+
420
+ ##
421
+ # Want an array of Schema::Attributes as well as a hash
422
+ # of all possible variations on a name pointing to correct array element.
423
+ #
424
+ def set_schema_attributes
425
+ @schema_attributes_array = []
426
+ @schema_attributes_hash = {}
427
+ UCB::LDAP::Schema.schema_hash[entity_name]["attributes"].each do |k, v|
428
+ sa = UCB::LDAP::Schema::Attribute.new(v.merge("name" => k))
429
+ @schema_attributes_array << sa
430
+ [sa.name, sa.aliases].flatten.each do |name|
431
+ @schema_attributes_hash[canonical(name)] = sa
432
+ end
433
+ end
434
+ rescue
435
+ raise "Error loading schema attributes for entity_name '#{entity_name}'"
436
+ end
437
+
438
+ ##
439
+ # Returns tree base for LDAP searches. Subclasses each have
440
+ # their own value.
441
+ #
442
+ # Can be overridden in #search by passing in a <tt>:base</tt> parm.
443
+ ##
444
+ def tree_base
445
+ @tree_base
446
+ end
447
+
448
+ def tree_base=(tree_base)
449
+ @tree_base = tree_base
450
+ end
451
+
452
+ end # end of class methods
453
+ end
454
+ end
455
+ end