Silence pychecker and pylint warnings
[public/pyceo-broken.git] / pylib / csc / adm / members.py
1 """
2 CSC Member Management
3
4 This module contains functions for registering new members, registering
5 members for terms, searching for members, and other member-related
6 functions.
7
8 Transactions are used in each method that modifies the database. 
9 Future changes to the members database that need to be atomic
10 must also be moved into this module.
11 """
12 import re, ldap
13 from csc.adm import terms
14 from csc.backends import ldapi
15 from csc.common import conf
16 from csc.common.excep import InvalidArgument
17
18
19 ### Configuration ###
20
21 CONFIG_FILE = '/etc/csc/members.cf'
22
23 cfg = {}
24
25 def load_configuration():
26     """Load Members Configuration"""
27
28     string_fields = [ 'realname_regex', 'server_url', 'users_base',
29             'groups_base', 'sasl_mech', 'sasl_realm', 'admin_bind_keytab',
30             'admin_bind_userid' ]
31
32     # read configuration file
33     cfg_tmp = conf.read(CONFIG_FILE)
34
35     # verify configuration
36     conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
37
38     # update the current configuration with the loaded values
39     cfg.update(cfg_tmp)
40
41
42
43 ### Exceptions ###
44
45 ConfigurationException = conf.ConfigurationException
46
47 class MemberException(Exception):
48     """Base exception class for member-related errors."""
49
50 class InvalidTerm(MemberException):
51     """Exception class for malformed terms."""
52     def __init__(self, term):
53         self.term = term
54     def __str__(self):
55         return "Term is invalid: %s" % self.term
56
57 class InvalidRealName(MemberException):
58     """Exception class for invalid real names."""
59     def __init__(self, name):
60         self.name = name
61     def __str__(self):
62         return "Name is invalid: %s" % self.name
63
64 class NoSuchMember(MemberException):
65     """Exception class for nonexistent members."""
66     def __init__(self, memberid):
67         self.memberid = memberid
68     def __str__(self):
69         return "Member not found: %d" % self.memberid
70
71
72
73 ### Connection Management ###
74
75 # global directory connection
76 ldap_connection = ldapi.LDAPConnection()
77
78 def connect():
79     """Connect to LDAP."""
80
81     load_configuration()
82     ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
83         cfg['sasl_realm'], cfg['admin_bind_userid'],
84         ('keytab', cfg['admin_bind_keytab']), cfg['users_base'],
85         cfg['groups_base'])
86
87 def disconnect():
88     """Disconnect from LDAP."""
89
90     ldap_connection.disconnect()
91
92
93 def connected():
94     """Determine whether the connection has been established."""
95
96     return ldap_connection.connected()
97
98
99
100 ### Member Table ###
101
102 def new(uid, realname, program=None):
103     """
104     Registers a new CSC member. The member is added to the members table
105     and registered for the current term.
106
107     Parameters:
108         uid       - the initial user id
109         realname  - the full real name of the member
110         program   - the program of study of the member
111
112     Returns: the username of the new member
113
114     Exceptions:
115         InvalidRealName    - if the real name is malformed
116
117     Example: new("Michael Spang", program="CS") -> "mspang"
118     """
119
120     # blank attributes should be NULL
121     if program == '': program = None
122     if uid == '': uid = None
123
124
125     # check real name format (UNIX account real names must not contain [,:=])
126     if not re.match(cfg['realname_regex'], realname):
127         raise InvalidRealName(realname)
128
129     # check for duplicate userid
130     member = ldap_connection.user_lookup(uid)
131     if member:
132         raise InvalidArgument("uid", uid, "duplicate uid")
133
134     # add the member to the directory
135     ldap_connection.member_add(uid, realname, program)
136
137     # register them for this term in the directory
138     member = ldap_connection.member_lookup(uid)
139     member['term'] = [ terms.current() ]
140     ldap_connection.user_modify(uid, member)
141
142     return uid
143
144
145 def get(userid):
146     """
147     Look up attributes of a member by userid.
148
149     Returns: a dictionary of attributes
150
151     Example: get('mspang') -> {
152                  'cn': [ 'Michael Spang' ],
153                  'program': [ 'Computer Science' ],
154                  ...
155              }
156     """
157
158     return ldap_connection.user_lookup(userid)
159
160
161 def list_term(term):
162     """
163     Build a list of members in a term.
164
165     Parameters:
166         term - the term to match members against
167
168     Returns: a list of members
169
170     Example: list_term('f2006'): -> {
171                  'mspang': { 'cn': 'Michael Spang', ... },
172                  'ctdalek': { 'cn': 'Calum T. Dalek', ... },
173                  ...
174              }
175     """
176
177     return ldap_connection.member_search_term(term)
178
179
180 def list_name(name):
181     """
182     Build a list of members with matching names.
183
184     Parameters:
185         name - the name to match members against
186
187     Returns: a list of member dictionaries
188
189     Example: list_name('Spang'): -> {
190                  'mspang': { 'cn': 'Michael Spang', ... },
191                  ...
192              ]
193     """
194
195     return ldap_connection.member_search_name(name)
196
197
198 def list_group(group):
199     """
200     Build a list of members in a group.
201
202     Parameters:
203         group - the group to match members against
204
205     Returns: a list of member dictionaries
206
207     Example: list_name('syscom'): -> {
208                  'mspang': { 'cn': 'Michael Spang', ... },
209                  ...
210              ]
211     """
212
213     members = group_members(group)
214     ret = {}
215     if members:
216         for member in members:
217             info = get(member)
218             if info:
219                 ret[member] = info
220     return ret
221
222
223 def list_positions():
224     """
225     Build a list of positions
226
227     Returns: a list of positions and who holds them
228
229     Example: list_positions(): -> {
230                  'president': { 'mspang': { 'cn': 'Michael Spang', ... } } ],
231                  ...
232              ]
233     """
234
235     ceo_ldap = ldap_connection.ldap
236     user_base = ldap_connection.user_base
237
238     members = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE, '(position=*)')
239     positions = {}
240     for (_, member) in members:
241         for position in member['position']:
242             if not position in positions:
243                 positions[position] = {}
244             positions[position][member['uid'][0]] = member
245     return positions
246
247 def set_position(position, members):
248     """
249     Sets a position
250
251     Parameters:
252         position - the position to set
253         members - an array of members that hold the position
254
255     Example: set_position('president', ['dtbartle'])
256     """
257
258     ceo_ldap = ldap_connection.ldap
259     user_base = ldap_connection.user_base
260     escape = ldap_connection.escape
261
262     res = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE,
263         '(&(objectClass=member)(position=%s))' % escape(position))
264     old = set([ member['uid'][0] for (_, member) in res ])
265     new = set(members)
266     mods = {
267         'del': set(old) - set(new),
268         'add': set(new) - set(old),
269     }
270     if len(mods['del']) == 0 and len(mods['add']) == 0:
271         return
272
273     for type in ['del', 'add']:
274         for userid in mods[type]:
275             dn = 'uid=%s,%s' % (escape(userid), user_base)
276             entry1 = {'position' : [position]}
277             entry2 = {} #{'position' : []}
278             entry = ()
279             if type == 'del':
280                 entry = (entry1, entry2)
281             elif type == 'add':
282                 entry = (entry2, entry1)
283             mlist = ldap_connection.make_modlist(entry[0], entry[1])
284             ceo_ldap.modify_s(dn, mlist)
285
286 def delete(userid):
287     """
288     Erase all records of a member.
289
290     Note: real members are never removed from the database
291
292     Returns: ldap entry of the member
293
294     Exceptions:
295         NoSuchMember - if the user id does not exist
296
297     Example: delete('ctdalek') -> { 'cn': [ 'Calum T. Dalek' ], 'term': ['s1993'], ... }
298     """
299
300     # save member data
301     member = ldap_connection.user_lookup(userid)
302
303     # bail if not found
304     if not member:
305         raise NoSuchMember(userid)
306
307     # remove data from the directory
308     uid = member['uid'][0]
309     ldap_connection.user_delete(uid)
310
311     return member
312
313
314 def change_group_member(action, group, userid):
315
316     ceo_ldap = ldap_connection.ldap
317     user_base = ldap_connection.user_base
318     group_base = ldap_connection.group_base
319     escape = ldap_connection.escape
320
321     user_dn = 'uid=%s,%s' % (escape(userid), user_base)
322     group_dn = 'cn=%s,%s' % (escape(group), group_base)
323     entry1 = {'uniqueMember' : []}
324     entry2 = {'uniqueMember' : [user_dn]}
325     entry = []
326     if action == 'add' or action == 'insert':
327         entry = (entry1, entry2)
328     elif action == 'remove' or action == 'delete':
329         entry = (entry2, entry1)
330     else:
331         raise InvalidArgument("action", action, "invalid action")
332     mlist = ldap_connection.make_modlist(entry[0], entry[1])
333     ceo_ldap.modify_s(group_dn, mlist)
334
335
336 ### Term Table ###
337
338 def register(userid, term_list):
339     """
340     Registers a member for one or more terms.
341
342     Parameters:
343         userid  - the member's username
344         term_list - the term to register for, or a list of terms
345
346     Exceptions:
347         InvalidTerm - if a term is malformed
348
349     Example: register(3349, "w2007")
350
351     Example: register(3349, ["w2007", "s2007"])
352     """
353
354     if type(term_list) in (str, unicode):
355         term_list = [ term_list ]
356
357     ldap_member = ldap_connection.member_lookup(userid)
358     if ldap_member and 'term' not in ldap_member:
359         ldap_member['term'] = []
360
361     if not ldap_member:
362         raise NoSuchMember(userid)
363
364     for term in term_list:
365
366         # check term syntax
367         if not re.match('^[wsf][0-9]{4}$', term):
368             raise InvalidTerm(term)
369
370         # add the term to the directory
371         ldap_member['term'].append(term)
372
373     ldap_connection.user_modify(userid, ldap_member)
374
375
376 def registered(userid, term):
377     """
378     Determines whether a member is registered
379     for a term.
380
381     Parameters:
382         userid   - the member's username
383         term     - the term to check
384
385     Returns: whether the member is registered
386
387     Example: registered("mspang", "f2006") -> True
388     """
389
390     member = ldap_connection.member_lookup(userid)
391     return 'term' in member and term in member['term']
392
393
394 def member_terms(userid):
395     """
396     Retrieves a list of terms a member is
397     registered for.
398
399     Parameters:
400         userid - the member's username
401
402     Returns: list of term strings
403
404     Example: registered('ctdalek') -> 's1993'
405     """
406
407     member = ldap_connection.member_lookup(userid)
408     if not 'term' in member:
409         return []
410     else:
411         return member['term']
412
413 def group_members(group):
414
415     """
416     Returns a list of group members
417     """
418
419     group = ldap_connection.group_lookup(group)
420     if group:
421         if 'uniqueMember' in group:
422             r = re.compile('^uid=([^,]*)')
423             return map(lambda x: r.match(x).group(1), group['uniqueMember'])
424         elif 'memberUid' in group:
425             return group['memberUid']
426         else:
427             return []
428     else:
429         return []
430
431
432 ### Tests ###
433
434 if __name__ == '__main__':
435
436     from csc.common.test import *
437
438     # t=test m=member u=updated
439     tmname = 'Test Member'
440     tmuid = 'testmember'
441     tmprogram = 'Metaphysics'
442     tm2name = 'Test Member 2'
443     tm2uid = 'testmember2'
444     tm2uname = 'Test Member II'
445     tm2uprogram = 'Pseudoscience'
446
447     tmdict = {'cn': [tmname], 'uid': [tmuid], 'program': [tmprogram] }
448     tm2dict = {'cn': [tm2name], 'uid': [tm2uid] }
449     tm2udict = {'cn': [tm2uname], 'uid': [tm2uid], 'program': [tm2uprogram] }
450
451     thisterm = terms.current()
452     nextterm = terms.next(thisterm)
453
454     test(connect)
455     connect()
456     success()
457
458     test(connected)
459     assert_equal(True, connected())
460     success()
461
462     test(new)
463     tmid = new(tmuid, tmname, tmprogram)
464     tm2id = new(tm2uid, tm2name)
465     success()
466
467     test(registered)
468     assert_equal(True, registered(tmid, thisterm))
469     assert_equal(True, registered(tm2id, thisterm))
470     assert_equal(False, registered(tmid, nextterm))
471     success()
472
473     test(get)
474     tmp = get(tmid)
475     del tmp['objectClass']
476     del tmp['term']
477     assert_equal(tmdict, tmp)
478     tmp = get(tm2id)
479     del tmp['objectClass']
480     del tmp['term']
481     assert_equal(tm2dict, tmp)
482     success()
483
484     test(list_name)
485     assert_equal(True, tmid in list_name(tmname).keys())
486     assert_equal(True, tm2id in list_name(tm2name).keys())
487     success()
488
489     test(register)
490     register(tmid, nextterm)
491     assert_equal(True, registered(tmid, nextterm))
492     success()
493
494     test(member_terms)
495     assert_equal([thisterm, nextterm], member_terms(tmid))
496     assert_equal([thisterm], member_terms(tm2id))
497     success()
498
499     test(list_term)
500     assert_equal(True, tmid in list_term(thisterm).keys())
501     assert_equal(True, tmid in list_term(nextterm).keys())
502     assert_equal(True, tm2id in list_term(thisterm).keys())
503     assert_equal(False, tm2id in list_term(nextterm).keys())
504     success()
505
506     test(get)
507     tmp = get(tm2id)
508     del tmp['objectClass']
509     del tmp['term']
510     assert_equal(tm2dict, tmp)
511     success()
512
513     test(delete)
514     delete(tmid)
515     delete(tm2id)
516     success()
517
518     test(disconnect)
519     disconnect()
520     assert_equal(False, connected())
521     disconnect()
522     success()