ucb_ldap 1.3.0

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.
@@ -0,0 +1,68 @@
1
+
2
+ module UCB::LDAP
3
+ module GenericAttributes
4
+
5
+
6
+ # Returns +true+ if the entry represents a test entry.
7
+ def test?
8
+ berkeleyEduTestIDFlag
9
+ end
10
+
11
+ def uid
12
+ super.first
13
+ end
14
+
15
+ def firstname
16
+ givenname.first
17
+ end
18
+ alias :first_name :firstname
19
+
20
+ def lastname
21
+ sn.first
22
+ end
23
+ alias :last_name :lastname
24
+
25
+ def email
26
+ mail.first
27
+ end
28
+
29
+ def phone
30
+ telephoneNumber.first
31
+ end
32
+
33
+ # Returns +Array+ of Affiliation for this Person. Requires a bind with access to affiliations.
34
+ # See UCB::LDAP.authenticate().
35
+ def affiliate_affiliations
36
+ @affiliate_affiliations ||= Affiliation.find_by_uid(uid)
37
+ end
38
+
39
+ # Returns +Array+ of Address for this Person.
40
+ # Requires a bind with access to addresses.
41
+ # See UCB::LDAP.authenticate().
42
+ def addresses
43
+ @addresses ||= Address.find_by_uid(uid)
44
+ end
45
+
46
+ # Returns +Array+ of Namespace for this Person.
47
+ # Requires a bind with access to namespaces.
48
+ # See UCB::LDAP.authenticate().
49
+ def namespaces
50
+ @namespaces ||= Namespace.find_by_uid(uid)
51
+ end
52
+
53
+ # Returns +Array+ of Service for this Person.
54
+ # Requires a bind with access to services.
55
+ # See UCB::LDAP.authenticate().
56
+ def services
57
+ @services ||= Service.find_by_uid(uid)
58
+ end
59
+
60
+ # Returns +Array+ of Address for this Person.
61
+ # Requires a bind with access to addresses.
62
+ # See UCB::LDAP.authenticate().
63
+ def addresses
64
+ @addresses ||= Address.find_by_uid(uid)
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,177 @@
1
+ #
2
+ require 'rubygems'
3
+ require 'net/ldap'
4
+ require 'time'
5
+ require 'ucb_ldap_exceptions'
6
+ require 'ucb_ldap_schema'
7
+ require 'ucb_ldap_schema_attribute'
8
+ require 'ucb_ldap_entry'
9
+
10
+ require 'person/affiliation_methods.rb'
11
+ require 'person/generic_attributes.rb'
12
+ require 'ucb_ldap_person.rb'
13
+
14
+ require 'ucb_ldap_person_job_appointment'
15
+ require 'ucb_ldap_org'
16
+ require 'ucb_ldap_namespace'
17
+ require 'ucb_ldap_address'
18
+ require 'ucb_ldap_student_term'
19
+ require 'ucb_ldap_affiliation'
20
+ require 'ucb_ldap_service'
21
+
22
+
23
+ module UCB #:nodoc:
24
+
25
+ # =UCB::LDAP
26
+ #
27
+ # <b>If you are doing searches that don't require a privileged bind
28
+ # and are accessing the default (production) server
29
+ # you probably don't need to call any of the methods in this module.</b>
30
+ #
31
+ # Methods in this module are about making <em>connections</em>
32
+ # to the LDAP directory.
33
+ #
34
+ # Interaction with the directory (searches and updates) is usually through the search()
35
+ # and other methods of UCB::LDAP::Entry and its sub-classes.
36
+ #
37
+ module LDAP
38
+
39
+
40
+ HOST_PRODUCTION = 'ldap.berkeley.edu'
41
+ HOST_TEST = 'ldap-test.berkeley.edu'
42
+
43
+
44
+ # class methods
45
+ class << self
46
+
47
+ # Give (new) bind credentials to LDAP. An attempt will be made
48
+ # to bind and will raise BindFailedException if bind fails.
49
+ #
50
+ # Call clear_authentication() to remove privileged bind.
51
+ def authenticate(username, password)
52
+ @username, @password = username, password
53
+ new_net_ldap # to force bind()
54
+ end
55
+
56
+ # Removes current bind (username, password).
57
+ def clear_authentication()
58
+ authenticate(nil, nil)
59
+ end
60
+
61
+ # Returns LDAP host used for lookups. Default is HOST_PRODUCTION.
62
+ def host()
63
+ @host || HOST_PRODUCTION
64
+ end
65
+
66
+ # Setter for #host.
67
+ #
68
+ # Note: validation of host is deferred until a search is performed
69
+ # or #authenticate() is called at which time a bad host will
70
+ # raise ConnectionFailedException.
71
+ #---
72
+ # Don't want to reconnect unless host really changed.
73
+ def host=(host)
74
+ if host != @host
75
+ @host = host
76
+ @net_ldap = nil
77
+ end
78
+ end
79
+
80
+ # Returns Net::LDAP instance that is used by UCB::LDAP::Entry
81
+ # and subclasses for directory searches.
82
+ #
83
+ # You might need this to perform searches not supported by
84
+ # sub-classes of Entry.
85
+ #
86
+ # Note: callers should not cache the results of this call unless they
87
+ # are prepared to handle timed-out connections (which this method does).
88
+ def net_ldap()
89
+ @net_ldap ||= new_net_ldap()
90
+ end
91
+
92
+ def password() #:nodoc:
93
+ @password
94
+ end
95
+
96
+ def username() #:nodoc:
97
+ @username
98
+ end
99
+
100
+ # If you are using UCB::LDAP in a Rails application you can specify binds on a
101
+ # per-environment basis, just as you can with database credentials.
102
+ #
103
+ # # in ../config/ldap.yml
104
+ #
105
+ # development:
106
+ # username: user_dev
107
+ # password: pass_dev
108
+ #
109
+ # # etc.
110
+ #
111
+ #
112
+ # # in ../config/environment.rb
113
+ #
114
+ # require 'ucb_ldap'
115
+ # UCB::LDAP.bind_for_rails()
116
+ #
117
+ # Runtime error will be raised if bind_file not found or if environment key not
118
+ # found in bind_file.
119
+ def bind_for_rails(bind_file = "#{RAILS_ROOT}/config/ldap.yml", environment = RAILS_ENV)
120
+ bind(bind_file, environment)
121
+ end
122
+
123
+ def bind(bind_file, environment)
124
+ raise "Can't find bind file: #{bind_file}" unless FileTest.exists?(bind_file)
125
+ binds = YAML.load(IO.read(bind_file))
126
+ bind = binds[environment] || raise("Can't find environment=#{environment} in bind file")
127
+ authenticate(bind['username'], bind['password'])
128
+ end
129
+
130
+ # Returns +arg+ as a Ruby +Date+ in local time zone. Returns +nil+ if +arg+ is +nil+.
131
+ def local_date_parse(arg)
132
+ arg.nil? ? nil : Date.parse(Time.parse(arg.to_s).localtime.to_s)
133
+ end
134
+
135
+ # Returns +arg+ as a Ruby +DateTime+ in local time zone. Returns +nil+ if +arg+ is +nil+.
136
+ def local_datetime_parse(arg)
137
+ arg.nil? ? nil : DateTime.parse(Time.parse(arg.to_s).localtime.to_s)
138
+ end
139
+
140
+ private unless $TESTING
141
+
142
+ # The value of the :auth parameter for Net::LDAP.new().
143
+ def authentication_information()
144
+ password.nil? ?
145
+ {:method => :anonymous} :
146
+ {:method => :simple, :username => username, :password => password}
147
+ end
148
+
149
+ # Returns new Net::LDAP instance.
150
+ # Note: Calling Net::LDAP.new does not result in a connection to the LDAP
151
+ # server. Rather, it stores the connection and binding parameters in the object.
152
+ # Later calls to: [search, add_attribute, rename_attribute, delete_attribute]
153
+ # will each result result in a new connection to the LDAP server.
154
+ def new_net_ldap()
155
+ @net_ldap = Net::LDAP.new(
156
+ :host => host,
157
+ :auth => authentication_information,
158
+ :port => 636,
159
+ :encryption => {:method =>:simple_tls}
160
+ )
161
+ raise(BindFailedException, @net_ldap.get_operation_result.to_s) unless @net_ldap.bind
162
+ @net_ldap
163
+ end
164
+
165
+ # Used for testing
166
+ def clear_instance_variables()
167
+ @host = nil
168
+ @net_ldap = nil
169
+ @username = nil
170
+ @password = nil
171
+ end
172
+
173
+ end
174
+
175
+ end
176
+ end
177
+
@@ -0,0 +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
@@ -0,0 +1,84 @@
1
+
2
+ module UCB
3
+ module LDAP
4
+ # = UCB::LDAP::Affiliation
5
+ #
6
+ # This class models a persons affiliate entries in the UCB LDAP directory.
7
+ #
8
+ # affiliations = Affiliation.find_by_uid("1234") #=> [#<UCB::LDAP::Affiliation: ...>, ...]
9
+ #
10
+ # Affiliation are usually loaded through a Person instance:
11
+ #
12
+ # p = Person.find_by_uid("1234") #=> #<UCB::LDAP::Person: ...>
13
+ # affs = p.affiliations #=> [#<UCB::LDAP::Affiliation: ...>, ...]
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 Affiliation search.
19
+ #
20
+ class Affiliation < Entry
21
+ @entity_name = 'personAffiliateAffiliation'
22
+
23
+ def create_datetime
24
+ berkeleyEduAffCreateDate
25
+ end
26
+
27
+ def expired_by
28
+ berkeleyEduAffExpBy
29
+ end
30
+
31
+ def expiration_date
32
+ UCB::LDAP.local_date_parse(berkeleyEduAffExpDate)
33
+ end
34
+
35
+ def affiliate_id
36
+ berkeleyEduAffID.first
37
+ end
38
+
39
+ def affiliate_type
40
+ berkeleyEduAffType
41
+ end
42
+
43
+ def first_name
44
+ givenName.first
45
+ end
46
+
47
+ def middle_name
48
+ berkeleyEduMiddleName
49
+ end
50
+
51
+ def last_name
52
+ sn.first
53
+ end
54
+
55
+ def modified_by
56
+ berkeleyEduModifiedBy
57
+ end
58
+
59
+ def source
60
+ berkeleyEduPersonAffiliateSource
61
+ end
62
+
63
+ def dept_code
64
+ departmentNumber.first
65
+ end
66
+
67
+ def dept_name
68
+ berkeleyEduUnitCalNetDeptName
69
+ end
70
+
71
+ class << self
72
+ # Returns an Array of Affiliation for <tt>uid</tt>.
73
+ # Returns an empty Array ([]) if nothing is found.
74
+ #
75
+ def find_by_uid(uid)
76
+ base = "uid=#{uid},ou=people,dc=berkeley,dc=edu"
77
+ filter = Net::LDAP::Filter.eq("objectclass", 'berkeleyEduPersonAffiliate')
78
+ search(:base => base, :filter => filter)
79
+ end
80
+
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,398 @@
1
+
2
+ module UCB
3
+ module LDAP
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 they 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
+ # Returns new instance of UCB::LDAP::Entry. The argument
81
+ # net_ldap_entry is an instance of Net::LDAP::Entry.
82
+ #
83
+ # You should not need to create any UCB::LDAP::Entry instances;
84
+ # they are created by calls to UCB::LDAP.search and friends.
85
+ def initialize(dn = nil) #:nodoc:
86
+ @new_record = true
87
+ @attributes = {}
88
+ @tainted_attributes = {}
89
+ end
90
+
91
+ def new_record?
92
+ @new_record
93
+ end
94
+
95
+ # Hydrates (populates) the object with values from the ldap resultset.
96
+ def self.hydrate(net_ldap_entry)
97
+ new_ldap_entry = self.new
98
+ new_ldap_entry.instance_variable_set(:@new_record, false)
99
+ # Don't store Net::LDAP entry in object since it uses the block
100
+ # initialization method of Hash which can't be marshalled ... this
101
+ # means it can't be stored in a Rails session.
102
+ net_ldap_entry.each do |attr, value|
103
+ new_ldap_entry.attributes[canonical(attr)] = value.map{|v| v.dup}
104
+ end
105
+ new_ldap_entry
106
+ end
107
+
108
+ def tainted_attributes
109
+ @tainted_attributes
110
+ end
111
+
112
+ # <tt>Hash</tt> of attributes returned from underlying NET::LDAP::Entry
113
+ # instance. Hash keys are #canonical attribute names, hash values are attribute
114
+ # values <em>as returned from LDAP</em>, i.e. arrays.
115
+ #
116
+ # You should most likely be referencing attributes as if they were
117
+ # instance methods rather than directly through this method. See top of
118
+ # this document.
119
+ def attributes
120
+ @attributes
121
+ end
122
+
123
+ # returns the value of the <em>distinguished name</em> attribute.
124
+ def dn
125
+ attributes[canonical(:dn)]
126
+ end
127
+
128
+ def canonical(string_or_symbol) #:nodoc:
129
+ self.class.canonical(string_or_symbol)
130
+ end
131
+
132
+ # update an existing entry. returns entry if successful else false.
133
+ #
134
+ # attrs = {:attr1 => "new_v1", :attr2 => "new_v2"}
135
+ # entry.update_attributes(attrs)
136
+ #
137
+ def update_attributes(attrs)
138
+ attrs.each {|k, v| self.send("#{k}=", v)}
139
+ if modify()
140
+ @attributes = self.class.find_by_dn(dn).attributes.dup
141
+ return true
142
+ end
143
+ false
144
+ end
145
+
146
+ # same as #update_attributes(), but raises directorynotupdated on failure.
147
+ def update_attributes!(attrs)
148
+ update_attributes(attrs) || raise(directorynotupdatedexception)
149
+ end
150
+
151
+ # delete entry. returns +true+ on sucess, +false+ on failure.
152
+ def delete
153
+ net_ldap.delete(:dn => dn)
154
+ end
155
+
156
+ # same as #delete() except raises directorynotupdated on failure.
157
+ def delete!
158
+ delete || raise(directorynotupdatedexception)
159
+ end
160
+
161
+ def net_ldap
162
+ self.class.net_ldap
163
+ end
164
+
165
+
166
+ private unless $testing
167
+
168
+ # used to get/set attribute values.
169
+ #
170
+ # if we can't make an attribute name out of method, let
171
+ # regular method_missing() handle it.
172
+ def method_missing(method, *args) #:nodoc:
173
+ setter_method?(method) ? value_setter(method, *args) : value_getter(method)
174
+ rescue BadAttributeNameException
175
+ return super
176
+ end
177
+
178
+ # returns +true+ if _method_ is a "setter", i.e., ends in "=".
179
+ def setter_method?(method)
180
+ method.to_s[-1, 1] == "="
181
+ end
182
+
183
+ # called by method_missing() to get an attribute value.
184
+ def value_getter(method)
185
+ schema_attribute = self.class.schema_attribute(method)
186
+ raw_value = attributes[canonical(schema_attribute.name)]
187
+ schema_attribute.get_value(raw_value)
188
+ end
189
+
190
+ # called by method_missing() to set an attribute value.
191
+ def value_setter(method, *args)
192
+ schema_attribute = self.class.schema_attribute(method.to_s.chop)
193
+ attr_key = canonical(schema_attribute.name)
194
+ tainted_attributes[attr_key] = schema_attribute.ldap_value(args[0])
195
+ end
196
+
197
+ def modify_operations
198
+ ops = []
199
+ tainted_attributes.keys.sort_by{|k| k.to_s}.each do |key|
200
+ value = tainted_attributes[key]
201
+ op = value.nil? ? :delete : :replace
202
+ ops << [op, key, value]
203
+ end
204
+ ops
205
+ end
206
+
207
+ def modify()
208
+ if ucb::ldap.net_ldap.modify(:dn => dn, :operations => modify_operations)
209
+ @tainted_attributes = nil
210
+ return true
211
+ end
212
+ false
213
+ end
214
+
215
+ # class methods
216
+ class << self
217
+
218
+ public
219
+
220
+ # creates and returns new entry. returns +false+ if unsuccessful.
221
+ # sets :objectclass key of <em>args[:attributes]</em> to
222
+ # object_classes read from schema.
223
+ #
224
+ # dn = "uid=999999,ou=people,dc=example,dc=com"
225
+ # attr = {
226
+ # :uid => "999999",
227
+ # :mail => "gsmith@example.com"
228
+ # }
229
+ #
230
+ # entrysubclass.create(:dn => dn, :attributes => attr) #=> #<ucb::ldap::entrysubclass ..>
231
+ #
232
+ # caller is responsible for setting :dn and :attributes correctly,
233
+ # as well as any other validation.
234
+ #
235
+ def create(args)
236
+ args[:attributes][:objectclass] = object_classes
237
+ net_ldap.add(args) or return false
238
+
239
+ # why is the object being refetched from ldap here?
240
+ find_by_dn(args[:dn])
241
+ end
242
+
243
+ def required_attributes
244
+ schema_attributes_hash.delete_if {|key, value| value["required"] == false }.keys
245
+ end
246
+
247
+ # returns entry whose distinguised name is _dn_.
248
+ def find_by_dn(dn)
249
+ search(
250
+ :base => dn,
251
+ :scope => Net::LDAP::SearchScope_BaseObject,
252
+ :filter => "objectClass=*"
253
+ ).first
254
+ end
255
+
256
+ # Same as #create(), but raises DirectoryNotUpdated on failure.
257
+ def create!(args)
258
+ create(args) || raise(DirectoryNotUpdatedException)
259
+ end
260
+
261
+ # Returns a new Net::LDAP::Filter that is the result of combining
262
+ # <em>filters</em> using <em>operator</em> (<em>filters</em> is
263
+ # an +Array+ of Net::LDAP::Filter).
264
+ #
265
+ # See Net::LDAP#& and Net::LDAP#| for details.
266
+ #
267
+ # f1 = Net::LDAP::Filter.eq("lastname", "hansen")
268
+ # f2 = Net::LDAP::Filter.eq("firstname", "steven")
269
+ #
270
+ # combine_filters([f1, f2]) # same as: f1 & f2
271
+ # combine_filters([f1, f2], '|') # same as: f1 | f2
272
+ #
273
+ def combine_filters(filters, operator = '&')
274
+ filters.inject{|accum, filter| accum.send(operator, filter)}
275
+ end
276
+
277
+ # Returns Net::LDAP::Filter. Allows for <em>filter</em> to
278
+ # be a +Hash+ of :key => value. Filters are combined with "&".
279
+ #
280
+ # UCB::LDAP::Entry.make_search_filter(:uid => '123')
281
+ # UCB::LDAP::Entry.make_search_filter(:a1 => v1, :a2 => v2)
282
+ #
283
+ def make_search_filter(filter)
284
+ return filter if filter.instance_of? Net::LDAP::Filter
285
+ return filter if filter.instance_of? String
286
+
287
+ filters = []
288
+ # sort so result is predictable for unit test
289
+ filter.keys.sort_by { |symbol| "#{symbol}" }.each do |attr|
290
+ filters << Net::LDAP::Filter.eq("#{attr}", "#{filter[attr]}")
291
+ end
292
+ combine_filters(filters, "&")
293
+ end
294
+
295
+ # Returns +Array+ of object classes making up this type of LDAP entity.
296
+ def object_classes
297
+ @object_classes ||= UCB::LDAP::Schema.schema_hash[entity_name]["objectClasses"]
298
+ end
299
+
300
+ def unique_object_class
301
+ @unique_object_class ||= UCB::LDAP::Schema.schema_hash[entity_name]["uniqueObjectClass"]
302
+ end
303
+
304
+ # Returns an +Array+ of Schema::Attribute for the entity.
305
+ def schema_attributes_array
306
+ @schema_attributes_array || set_schema_attributes
307
+ @schema_attributes_array
308
+ end
309
+
310
+ # Returns as +Hash+ whose keys are the canonical attribute names
311
+ # and whose values are the corresponding Schema::Attributes.
312
+ def schema_attributes_hash
313
+ @schema_attributes_hash || set_schema_attributes
314
+ @schema_attributes_hash
315
+ end
316
+
317
+ def schema_attribute(attribute_name)
318
+ schema_attributes_hash[canonical(attribute_name)] ||
319
+ raise(BadAttributeNameException, "'#{attribute_name}' is not a recognized attribute name")
320
+ end
321
+
322
+ # Returns Array of UCB::LDAP::Entry for entries matching _args_.
323
+ # When called from a subclass, returns Array of subclass instances.
324
+ #
325
+ # See Net::LDAP::search for more information on _args_.
326
+ #
327
+ # Most common arguments are <tt>:base</tt> and <tt>:filter</tt>.
328
+ # Search methods of subclasses have default <tt>:base</tt> that
329
+ # can be overriden.
330
+ #
331
+ # See make_search_filter for <tt>:filter</tt> options.
332
+ #
333
+ # base = "ou=people,dc=berkeley,dc=edu"
334
+ # entries = UCB::LDAP::Entry.search(:base => base, :filter => {:uid => '123'})
335
+ # entries = UCB::LDAP::Entry.search(:base => base, :filter => {:sn => 'Doe', :givenname => 'John'}
336
+ #
337
+ def search(args={})
338
+ args = args.dup
339
+ args[:base] ||= tree_base
340
+ args[:filter] = make_search_filter args[:filter] if args[:filter]
341
+
342
+ results = []
343
+ net_ldap.search(args) do |entry|
344
+ results << hydrate(entry)
345
+ end
346
+ results
347
+ end
348
+
349
+ # Returns the canonical representation of a symbol or string so
350
+ # we can look up attributes in a number of ways.
351
+ def canonical(string_or_symbol)
352
+ string_or_symbol.to_s.downcase.to_sym
353
+ end
354
+
355
+ # Returns underlying Net::LDAP instance.
356
+ def net_ldap #:nodoc:
357
+ UCB::LDAP.net_ldap
358
+ end
359
+
360
+ private unless $TESTING
361
+
362
+ # Schema entity name. Set in each subclass.
363
+ def entity_name
364
+ @entity_name
365
+ end
366
+
367
+ # Want an array of Schema::Attributes as well as a hash
368
+ # of all possible variations on a name pointing to correct array element.
369
+ def set_schema_attributes
370
+ @schema_attributes_array = []
371
+ @schema_attributes_hash = {}
372
+ UCB::LDAP::Schema.schema_hash[entity_name]["attributes"].each do |k, v|
373
+ sa = UCB::LDAP::Schema::Attribute.new(v.merge("name" => k))
374
+ @schema_attributes_array << sa
375
+ [sa.name, sa.aliases].flatten.each do |name|
376
+ @schema_attributes_hash[canonical(name)] = sa
377
+ end
378
+ end
379
+ rescue
380
+ raise "Error loading schema attributes for entity_name '#{entity_name}'"
381
+ end
382
+
383
+ # Returns tree base for LDAP searches. Subclasses each have
384
+ # their own value.
385
+ #
386
+ # Can be overridden in #search by passing in a <tt>:base</tt> parm.
387
+ def tree_base
388
+ @tree_base
389
+ end
390
+
391
+ def tree_base=(tree_base)
392
+ @tree_base = tree_base
393
+ end
394
+
395
+ end # end of class methods
396
+ end
397
+ end
398
+ end