ucb_ldap 2.0.0.pre1 → 2.0.0.pre3

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