Add preliminary group management functions
[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 delete(userid):
197     """
198     Erase all records of a member.
199
200     Note: real members are never removed from the database
201
202     Returns: ldap entry of the member
203
204     Exceptions:
205         NoSuchMember - if the user id does not exist
206
207     Example: delete('ctdalek') -> { 'cn': [ 'Calum T. Dalek' ], 'term': ['s1993'], ... }
208     """
209
210     # save member data
211     member = ldap_connection.user_lookup(userid)
212
213     # bail if not found
214     if not member:
215         raise NoSuchMember(userid)
216
217     # remove data from the directory
218     uid = member['uid'][0]
219     ldap_connection.user_delete(uid)
220
221     return member
222
223
224
225 ### Term Table ###
226
227 def register(userid, term_list):
228     """
229     Registers a member for one or more terms.
230
231     Parameters:
232         userid  - the member's username
233         term_list - the term to register for, or a list of terms
234
235     Exceptions:
236         InvalidTerm - if a term is malformed
237
238     Example: register(3349, "w2007")
239
240     Example: register(3349, ["w2007", "s2007"])
241     """
242
243     if type(term_list) in (str, unicode):
244         term_list = [ term_list ]
245
246     ldap_member = ldap_connection.member_lookup(userid)
247     if ldap_member and 'term' not in ldap_member:
248         ldap_member['term'] = []
249
250     if not ldap_member:
251         raise NoSuchMember(userid)
252
253     for term in term_list:
254
255         # check term syntax
256         if not re.match('^[wsf][0-9]{4}$', term):
257             raise InvalidTerm(term)
258
259         # add the term to the directory
260         ldap_member['term'].append(term)
261
262     ldap_connection.user_modify(userid, ldap_member)
263
264
265 def registered(userid, term):
266     """
267     Determines whether a member is registered
268     for a term.
269
270     Parameters:
271         userid   - the member's username
272         term     - the term to check
273
274     Returns: whether the member is registered
275
276     Example: registered("mspang", "f2006") -> True
277     """
278
279     member = ldap_connection.member_lookup(userid)
280     return 'term' in member and term in member['term']
281
282
283 def member_terms(userid):
284     """
285     Retrieves a list of terms a member is
286     registered for.
287
288     Parameters:
289         userid - the member's username
290
291     Returns: list of term strings
292
293     Example: registered('ctdalek') -> 's1993'
294     """
295
296     member = ldap_connection.member_lookup(userid)
297     if not 'term' in member:
298         return []
299     else:
300         return member['term']
301
302 def group_members(group):
303
304     """
305     Returns a list of group members
306     """
307
308     group = ldap_connection.group_lookup(group)
309     if not 'uniqueMember' in group:
310         return []
311     else:
312         return group['uniqueMember']
313
314
315 ### Tests ###
316
317 if __name__ == '__main__':
318
319     from csc.common.test import *
320
321     # t=test m=member u=updated
322     tmname = 'Test Member'
323     tmuid = 'testmember'
324     tmprogram = 'Metaphysics'
325     tm2name = 'Test Member 2'
326     tm2uid = 'testmember2'
327     tm2uname = 'Test Member II'
328     tm2uprogram = 'Pseudoscience'
329
330     tmdict = {'cn': [tmname], 'uid': [tmuid], 'program': [tmprogram] }
331     tm2dict = {'cn': [tm2name], 'uid': [tm2uid] }
332     tm2udict = {'cn': [tm2uname], 'uid': [tm2uid], 'program': [tm2uprogram] }
333
334     thisterm = terms.current()
335     nextterm = terms.next(thisterm)
336
337     test(connect)
338     connect()
339     success()
340
341     test(connected)
342     assert_equal(True, connected())
343     success()
344
345     test(new)
346     tmid = new(tmuid, tmname, tmprogram)
347     tm2id = new(tm2uid, tm2name)
348     success()
349
350     test(registered)
351     assert_equal(True, registered(tmid, thisterm))
352     assert_equal(True, registered(tm2id, thisterm))
353     assert_equal(False, registered(tmid, nextterm))
354     success()
355
356     test(get)
357     tmp = get(tmid)
358     del tmp['objectClass']
359     del tmp['term']
360     assert_equal(tmdict, tmp)
361     tmp = get(tm2id)
362     del tmp['objectClass']
363     del tmp['term']
364     assert_equal(tm2dict, tmp)
365     success()
366
367     test(list_name)
368     assert_equal(True, tmid in list_name(tmname).keys())
369     assert_equal(True, tm2id in list_name(tm2name).keys())
370     success()
371
372     test(register)
373     register(tmid, nextterm)
374     assert_equal(True, registered(tmid, nextterm))
375     success()
376
377     test(member_terms)
378     assert_equal([thisterm, nextterm], member_terms(tmid))
379     assert_equal([thisterm], member_terms(tm2id))
380     success()
381
382     test(list_term)
383     assert_equal(True, tmid in list_term(thisterm).keys())
384     assert_equal(True, tmid in list_term(nextterm).keys())
385     assert_equal(True, tm2id in list_term(thisterm).keys())
386     assert_equal(False, tm2id in list_term(nextterm).keys())
387     success()
388
389     test(get)
390     tmp = get(tm2id)
391     del tmp['objectClass']
392     del tmp['term']
393     assert_equal(tm2dict, tmp)
394     success()
395
396     test(delete)
397     delete(tmid)
398     delete(tm2id)
399     success()
400
401     test(disconnect)
402     disconnect()
403     assert_equal(False, connected())
404     disconnect()
405     success()