Merge branch 'master' of /users/git/public/pyceo
[mspang/pyceo.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
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',
29             'database', 'user', 'password', 'server_url', 'users_base',
30             'groups_base', 'admin_bind_dn', 'admin_bind_pw' ]
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(cfg['server_url'], cfg['admin_bind_dn'], cfg['admin_bind_pw'], cfg['users_base'], cfg['groups_base'])
83
84
85 def disconnect():
86     """Disconnect from LDAP."""
87
88     ldap_connection.disconnect()
89
90
91 def connected():
92     """Determine whether the connection has been established."""
93
94     return ldap_connection.connected()
95
96
97
98 ### Member Table ###
99
100 def new(uid, realname, program=None):
101     """
102     Registers a new CSC member. The member is added to the members table
103     and registered for the current term.
104
105     Parameters:
106         uid       - the initial user id
107         realname  - the full real name of the member
108         program   - the program of study of the member
109
110     Returns: the username of the new member
111
112     Exceptions:
113         InvalidRealName    - if the real name is malformed
114
115     Example: new("Michael Spang", program="CS") -> "mspang"
116     """
117
118     # blank attributes should be NULL
119     if program == '': program = None
120     if uid == '': uid = None
121
122
123     # check real name format (UNIX account real names must not contain [,:=])
124     if not re.match(cfg['realname_regex'], realname):
125         raise InvalidRealName(realname)
126
127     # check for duplicate userid
128     member = ldap_connection.user_lookup(uid)
129     if member:
130         raise InvalidArgument("uid", uid, "duplicate uid")
131
132     # add the member to the directory
133     ldap_connection.member_add(uid, realname, program)
134
135     # register them for this term in the directory
136     member = ldap_connection.member_lookup(uid)
137     member['term'] = [ terms.current() ]
138     ldap_connection.user_modify(uid, member)
139
140     return uid
141
142
143 def get(userid):
144     """
145     Look up attributes of a member by userid.
146
147     Returns: a dictionary of attributes
148
149     Example: get('mspang') -> {
150                  'cn': [ 'Michael Spang' ],
151                  'program': [ 'Computer Science' ],
152                  ...
153              }
154     """
155
156     return ldap_connection.user_lookup(userid)
157
158
159 def list_term(term):
160     """
161     Build a list of members in a term.
162
163     Parameters:
164         term - the term to match members against
165
166     Returns: a list of members
167
168     Example: list_term('f2006'): -> {
169                  'mspang': { 'cn': 'Michael Spang', ... },
170                  'ctdalek': { 'cn': 'Calum T. Dalek', ... },
171                  ...
172              }
173     """
174
175     return ldap_connection.member_search_term(term)
176
177
178 def list_name(name):
179     """
180     Build a list of members with matching names.
181
182     Parameters:
183         name - the name to match members against
184
185     Returns: a list of member dictionaries
186
187     Example: list_name('Spang'): -> {
188                  'mspang': { 'cn': 'Michael Spang', ... },
189                  ...
190              ]
191     """
192
193     return ldap_connection.member_search_name(name)
194
195
196 def list_group(group):
197     """
198     Build a list of members in a group.
199
200     Parameters:
201         group - the group to match members against
202
203     Returns: a list of member dictionaries
204
205     Example: list_name('syscom'): -> {
206                  'mspang': { 'cn': 'Michael Spang', ... },
207                  ...
208              ]
209     """
210
211     members = group_members(group)
212     if members:
213         ret = {}
214         for member in members:
215             info = get(member)
216             if info:
217                 ret[member] = info
218         return ret
219     else:
220         return {}
221
222
223 def delete(userid):
224     """
225     Erase all records of a member.
226
227     Note: real members are never removed from the database
228
229     Returns: ldap entry of the member
230
231     Exceptions:
232         NoSuchMember - if the user id does not exist
233
234     Example: delete('ctdalek') -> { 'cn': [ 'Calum T. Dalek' ], 'term': ['s1993'], ... }
235     """
236
237     # save member data
238     member = ldap_connection.user_lookup(userid)
239
240     # bail if not found
241     if not member:
242         raise NoSuchMember(userid)
243
244     # remove data from the directory
245     uid = member['uid'][0]
246     ldap_connection.user_delete(uid)
247
248     return member
249
250
251
252 ### Term Table ###
253
254 def register(userid, term_list):
255     """
256     Registers a member for one or more terms.
257
258     Parameters:
259         userid  - the member's username
260         term_list - the term to register for, or a list of terms
261
262     Exceptions:
263         InvalidTerm - if a term is malformed
264
265     Example: register(3349, "w2007")
266
267     Example: register(3349, ["w2007", "s2007"])
268     """
269
270     if type(term_list) in (str, unicode):
271         term_list = [ term_list ]
272
273     ldap_member = ldap_connection.member_lookup(userid)
274     if ldap_member and 'term' not in ldap_member:
275         ldap_member['term'] = []
276
277     if not ldap_member:
278         raise NoSuchMember(userid)
279
280     for term in term_list:
281
282         # check term syntax
283         if not re.match('^[wsf][0-9]{4}$', term):
284             raise InvalidTerm(term)
285
286         # add the term to the directory
287         ldap_member['term'].append(term)
288
289     ldap_connection.user_modify(userid, ldap_member)
290
291
292 def registered(userid, term):
293     """
294     Determines whether a member is registered
295     for a term.
296
297     Parameters:
298         userid   - the member's username
299         term     - the term to check
300
301     Returns: whether the member is registered
302
303     Example: registered("mspang", "f2006") -> True
304     """
305
306     member = ldap_connection.member_lookup(userid)
307     return 'term' in member and term in member['term']
308
309
310 def member_terms(userid):
311     """
312     Retrieves a list of terms a member is
313     registered for.
314
315     Parameters:
316         userid - the member's username
317
318     Returns: list of term strings
319
320     Example: registered('ctdalek') -> 's1993'
321     """
322
323     member = ldap_connection.member_lookup(userid)
324     if not 'term' in member:
325         return []
326     else:
327         return member['term']
328
329 def group_members(group):
330
331     """
332     Returns a list of group members
333     """
334
335     group = ldap_connection.group_lookup(group)
336     if group:
337         if 'uniqueMember' in group:
338             r = re.compile('^uid=([^,]*)')
339             return map(lambda x: r.match(x).group(1), group['uniqueMember'])
340         elif 'memberUid' in group:
341             return group['memberUid']
342         else:
343             return []
344     else:
345         return []
346
347
348 ### Tests ###
349
350 if __name__ == '__main__':
351
352     from csc.common.test import *
353
354     # t=test m=member u=updated
355     tmname = 'Test Member'
356     tmuid = 'testmember'
357     tmprogram = 'Metaphysics'
358     tm2name = 'Test Member 2'
359     tm2uid = 'testmember2'
360     tm2uname = 'Test Member II'
361     tm2uprogram = 'Pseudoscience'
362
363     tmdict = {'cn': [tmname], 'uid': [tmuid], 'program': [tmprogram] }
364     tm2dict = {'cn': [tm2name], 'uid': [tm2uid] }
365     tm2udict = {'cn': [tm2uname], 'uid': [tm2uid], 'program': [tm2uprogram] }
366
367     thisterm = terms.current()
368     nextterm = terms.next(thisterm)
369
370     test(connect)
371     connect()
372     success()
373
374     test(connected)
375     assert_equal(True, connected())
376     success()
377
378     test(new)
379     tmid = new(tmuid, tmname, tmprogram)
380     tm2id = new(tm2uid, tm2name)
381     success()
382
383     test(registered)
384     assert_equal(True, registered(tmid, thisterm))
385     assert_equal(True, registered(tm2id, thisterm))
386     assert_equal(False, registered(tmid, nextterm))
387     success()
388
389     test(get)
390     tmp = get(tmid)
391     del tmp['objectClass']
392     del tmp['term']
393     assert_equal(tmdict, tmp)
394     tmp = get(tm2id)
395     del tmp['objectClass']
396     del tmp['term']
397     assert_equal(tm2dict, tmp)
398     success()
399
400     test(list_name)
401     assert_equal(True, tmid in list_name(tmname).keys())
402     assert_equal(True, tm2id in list_name(tm2name).keys())
403     success()
404
405     test(register)
406     register(tmid, nextterm)
407     assert_equal(True, registered(tmid, nextterm))
408     success()
409
410     test(member_terms)
411     assert_equal([thisterm, nextterm], member_terms(tmid))
412     assert_equal([thisterm], member_terms(tm2id))
413     success()
414
415     test(list_term)
416     assert_equal(True, tmid in list_term(thisterm).keys())
417     assert_equal(True, tmid in list_term(nextterm).keys())
418     assert_equal(True, tm2id in list_term(thisterm).keys())
419     assert_equal(False, tm2id in list_term(nextterm).keys())
420     success()
421
422     test(get)
423     tmp = get(tm2id)
424     del tmp['objectClass']
425     del tmp['term']
426     assert_equal(tm2dict, tmp)
427     success()
428
429     test(delete)
430     delete(tmid)
431     delete(tm2id)
432     success()
433
434     test(disconnect)
435     disconnect()
436     assert_equal(False, connected())
437     disconnect()
438     success()