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.
- data/CHANGELOG +127 -0
- data/Manifest +25 -0
- data/README +80 -0
- data/Rakefile +22 -0
- data/TODO +2 -0
- data/init.rb +1 -0
- data/lib/person/adv_con_person.rb +0 -0
- data/lib/person/affiliation_methods.rb +212 -0
- data/lib/person/generic_attributes.rb +68 -0
- data/lib/ucb_ldap.rb +177 -0
- data/lib/ucb_ldap_address.rb +106 -0
- data/lib/ucb_ldap_affiliation.rb +84 -0
- data/lib/ucb_ldap_entry.rb +398 -0
- data/lib/ucb_ldap_exceptions.rb +27 -0
- data/lib/ucb_ldap_namespace.rb +42 -0
- data/lib/ucb_ldap_org.rb +369 -0
- data/lib/ucb_ldap_person.rb +135 -0
- data/lib/ucb_ldap_person_job_appointment.rb +79 -0
- data/lib/ucb_ldap_schema.rb +115 -0
- data/lib/ucb_ldap_schema_attribute.rb +153 -0
- data/lib/ucb_ldap_service.rb +109 -0
- data/lib/ucb_ldap_student_term.rb +101 -0
- data/lib/ucb_simple_ldap_entry.rb +201 -0
- data/schema/schema.yml +2954 -0
- data/ucb_ldap.gemspec +40 -0
- data/version.yml +1 -0
- metadata +124 -0
@@ -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
|
data/lib/ucb_ldap.rb
ADDED
@@ -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
|