23e2ee03fbb8f4cd7f6a8104fc72af5272b8e9ec
[mspang/pyceo.git] / pylib / csc / backends / ldapi.py
1 """
2 LDAP Backend Interface
3
4 This module is intended to be a thin wrapper around LDAP operations.
5 Methods on the connection object correspond in a straightforward way
6 to LDAP queries and updates.
7
8 A LDAP entry is the most important component of a CSC UNIX account.
9 The entry contains the username, user id number, real name, shell,
10 and other important information. All non-local UNIX accounts must
11 have an LDAP entry, even if the account does not log in directly.
12
13 This module makes use of python-ldap, a Python module with bindings
14 to libldap, OpenLDAP's native C client library.
15 """
16 import ldap.modlist
17 from subprocess import Popen, PIPE
18
19
20 class LDAPException(Exception):
21     """Exception class for LDAP-related errors."""
22
23
24 class LDAPConnection(object):
25     """
26     Connection to the LDAP directory. All directory
27     queries and updates are made via this class.
28
29     Exceptions: (all methods)
30         LDAPException - on directory query failure
31
32     Example:
33          connection = LDAPConnection()
34          connection.connect(...)
35
36          # make queries and updates, e.g.
37          connection.user_delete('mspang')
38
39          connection.disconnect()
40     """
41
42     def __init__(self):
43         self.ldap = None
44
45
46     def connect_anon(self, uri, user_base, group_base):
47         """
48         Establish a connection to the LDAP Server.
49
50         Parameters:
51             uri        - connection string (e.g. ldap://foo.com, ldaps://bar.com)
52             user_base  - base of the users subtree
53             group_base - baes of the group subtree
54
55         Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
56                      'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
57                      'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
58
59         """
60
61         # open the connection
62         self.ldap = ldap.initialize(uri)
63
64         # authenticate
65         self.ldap.simple_bind_s('', '')
66
67         self.user_base = user_base
68         self.group_base = group_base
69
70
71     def connect_sasl(self, uri, mech, realm, user_base, group_base):
72
73         # open the connection
74         self.ldap = ldap.initialize(uri)
75
76         # authenticate
77         sasl = Sasl(mech, realm)
78         self.ldap.sasl_interactive_bind_s('', sasl)
79
80         self.user_base = user_base
81         self.group_base = group_base
82
83
84     def disconnect(self):
85         """Close the connection to the LDAP server."""
86         
87         if self.ldap:
88
89             # close connection
90             try:
91                 self.ldap.unbind_s()
92                 self.ldap = None
93             except ldap.LDAPError, e:
94                 raise LDAPException("unable to disconnect: %s" % e)
95
96
97     def connected(self):
98         """Determine whether the connection has been established."""
99
100         return self.ldap is not None
101
102
103
104     ### Helper Methods ###
105
106     def lookup(self, dn, objectClass=None):
107         """
108         Helper method to retrieve the attributes of an entry.
109
110         Parameters:
111             dn - the distinguished name of the directory entry
112
113         Returns: a dictionary of attributes of the matched dn, or
114                  None of the dn does not exist in the directory
115         """
116
117         if not self.connected(): raise LDAPException("Not connected!")
118
119         # search for the specified dn
120         try:
121             if objectClass:
122                 search_filter = '(objectClass=%s)' % self.escape(objectClass)
123                 matches = self.ldap.search_s(dn, ldap.SCOPE_BASE, search_filter)
124             else:
125                 matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
126         except ldap.NO_SUCH_OBJECT:
127             return None
128         except ldap.LDAPError, e:
129             raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
130             
131         # this should never happen due to the nature of DNs
132         if len(matches) > 1:
133             raise LDAPException("duplicate dn in ldap: " + dn)
134
135         # dn was found, but didn't match the objectClass filter
136         elif len(matches) < 1:
137             return None
138
139         # return the attributes of the single successful match
140         match = matches[0]
141         match_dn, match_attributes = match
142         return match_attributes
143
144
145
146     ### User-related Methods ###
147
148     def user_lookup(self, uid, objectClass=None):
149         """
150         Retrieve the attributes of a user.
151
152         Parameters:
153             uid - the uid to look up
154
155         Returns: attributes of user with uid
156         """
157
158         dn = 'uid=' + uid + ',' + self.user_base
159         return self.lookup(dn, objectClass)
160
161
162     def user_search(self, search_filter, params):
163         """
164         Search for users with a filter.
165
166         Parameters:
167             search_filter - LDAP filter string to match users against
168
169         Returns: a dictionary mapping uids to attributes
170         """
171
172         if not self.connected(): raise LDAPException("Not connected!")
173
174         search_filter = search_filter % tuple(self.escape(x) for x in params)
175
176         # search for entries that match the filter
177         try:
178             matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
179         except ldap.LDAPError, e:
180             raise LDAPException("user search failed: %s" % e)
181
182         results = {}
183         for match in matches:
184             dn, attrs = match
185             uid = attrs['uid'][0]
186             results[uid] = attrs
187
188         return results
189
190
191     def user_modify(self, uid, attrs):
192         """
193         Update user attributes in the directory.
194
195         Parameters:
196             uid   - username of the user to modify
197             attrs - dictionary as returned by user_lookup() with changes to make.
198                     omitted attributes are DELETED.
199
200         Example: user = user_lookup('mspang')
201                  user['uidNumber'] = [ '0' ]
202                  connection.user_modify('mspang', user)
203         """
204
205         # distinguished name of the entry to modify
206         dn = 'uid=' + uid + ',' + self.user_base
207
208         # retrieve current state of user
209         old_user = self.user_lookup(uid)
210
211         try:
212
213             # build list of modifications to make
214             changes = ldap.modlist.modifyModlist(old_user, attrs)
215
216             # apply changes
217             self.ldap.modify_s(dn, changes)
218
219         except ldap.LDAPError, e:
220             raise LDAPException("unable to modify: %s" % e)
221
222
223
224     ### Group-related Methods ###
225
226     def group_lookup(self, cn):
227         """
228         Retrieves the attributes of a group.
229
230         Parameters:
231             cn - the UNIX group name to lookup
232
233         Returns: attributes of the group's LDAP entry
234
235         Example: connection.group_lookup('office') -> {
236                      'cn': 'office',
237                      'gidNumber', '1001',
238                      ...
239                  }
240         """
241
242         dn = 'cn=' + cn + ',' + self.group_base
243         return self.lookup(dn, 'posixGroup')
244
245
246     ### Member-related Methods ###
247
248     def member_lookup(self, uid):
249         """
250         Retrieve the attributes of a member. This method will only return
251         results that have the objectClass 'member'.
252
253         Parameters:
254             uid - the username to look up
255
256         Returns: attributes of member with uid
257
258         Example: connection.member_lookup('mspang') ->
259                      { 'uid': 'mspang', 'uidNumber': 21292 ...}
260         """
261
262         if not self.connected(): raise LDAPException("Not connected!")
263
264         dn = 'uid=' + uid + ',' + self.user_base
265         return self.lookup(dn, 'member')
266
267
268     def member_search_name(self, name):
269         """
270         Retrieves a list of members with the specified name (fuzzy).
271
272         Returns: a dictionary mapping uids to attributes
273         """
274
275         search_filter = '(&(objectClass=member)(cn~=%s))'
276         return self.user_search(search_filter, [ name ] )
277
278
279     def member_search_term(self, term):
280         """
281         Retrieves a list of members who were registered in a certain term.
282
283         Returns: a dictionary mapping uids to attributes
284         """
285
286         search_filter = '(&(objectClass=member)(term=%s))'
287         return self.user_search(search_filter, [ term ])
288
289
290     def member_search_program(self, program):
291         """
292         Retrieves a list of members in a certain program (fuzzy).
293
294         Returns: a dictionary mapping uids to attributes
295         """
296
297         search_filter = '(&(objectClass=member)(program~=%s))'
298         return self.user_search(search_filter, [ program ])
299
300
301     def member_add(self, uid, cn, program=None, description=None):
302         """
303         Adds a member to the directory.
304
305         Parameters:
306             uid           - the UNIX username for the member
307             cn            - the real name of the member
308             program       - the member's program of study
309             description   - a description for the entry
310         """
311
312         dn = 'uid=' + uid + ',' + self.user_base
313         attrs = {
314             'objectClass': [ 'top', 'account', 'member' ],
315             'uid': [ uid ],
316             'cn': [ cn ],
317         }
318
319         if program:
320             attrs['program'] = [ program ]
321         if description:
322             attrs['description'] = [ description ]
323
324         try:
325             modlist = ldap.modlist.addModlist(attrs)
326             self.ldap.add_s(dn, modlist)
327         except ldap.LDAPError, e:
328             raise LDAPException("unable to add: %s" % e)
329
330
331
332     ### Miscellaneous Methods ###
333
334     def escape(self, value):
335         """
336         Escapes special characters in a value so that it may be safely inserted
337         into an LDAP search filter.
338         """
339
340         value = str(value)
341         value = value.replace('\\', '\\5c').replace('*', '\\2a')
342         value = value.replace('(', '\\28').replace(')', '\\29')
343         value = value.replace('\x00', '\\00')
344         return value
345
346
347     def make_modlist(self, old, new):
348         keys = set(old.keys()).union(set(new))
349         mlist = []
350         for key in keys:
351             if key in old and not key in new:
352                 mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
353             elif key in new and not key in old:
354                 mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
355             else:
356                 to_add = list(set(new[key]) - set(old[key]))
357                 if len(to_add) > 0:
358                     mlist.append((ldap.MOD_ADD, key, to_add))
359                 to_del = list(set(old[key]) - set(new[key]))
360                 if len(to_del) > 0:
361                     mlist.append((ldap.MOD_DELETE, key, to_del))
362         return mlist
363
364
365 class Sasl:
366
367     def __init__(self, mech, realm):
368         self.mech = mech
369         self.realm = realm
370
371     def callback(self, id, challenge, prompt, defresult):
372         return ''