27ff083c93737f833a4557c8820112f432df1ea8
[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, 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 NoSuchMember(MemberException):
58     """Exception class for nonexistent members."""
59     def __init__(self, memberid):
60         self.memberid = memberid
61     def __str__(self):
62         return "Member not found: %d" % self.memberid
63
64
65
66 ### Connection Management ###
67
68 # global directory connection
69 ldap_connection = ldapi.LDAPConnection()
70
71 def connect():
72     """Connect to LDAP."""
73
74     load_configuration()
75     ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
76         cfg['sasl_realm'], cfg['admin_bind_userid'],
77         ('keytab', cfg['admin_bind_keytab']), cfg['users_base'],
78         cfg['groups_base'])
79
80 def disconnect():
81     """Disconnect from LDAP."""
82
83     ldap_connection.disconnect()
84
85
86 def connected():
87     """Determine whether the connection has been established."""
88
89     return ldap_connection.connected()
90
91
92
93 ### Members ###
94
95 def get(userid):
96     """
97     Look up attributes of a member by userid.
98
99     Returns: a dictionary of attributes
100
101     Example: get('mspang') -> {
102                  'cn': [ 'Michael Spang' ],
103                  'program': [ 'Computer Science' ],
104                  ...
105              }
106     """
107
108     return ldap_connection.user_lookup(userid)
109
110
111 def list_term(term):
112     """
113     Build a list of members in a term.
114
115     Parameters:
116         term - the term to match members against
117
118     Returns: a list of members
119
120     Example: list_term('f2006'): -> {
121                  'mspang': { 'cn': 'Michael Spang', ... },
122                  'ctdalek': { 'cn': 'Calum T. Dalek', ... },
123                  ...
124              }
125     """
126
127     return ldap_connection.member_search_term(term)
128
129
130 def list_name(name):
131     """
132     Build a list of members with matching names.
133
134     Parameters:
135         name - the name to match members against
136
137     Returns: a list of member dictionaries
138
139     Example: list_name('Spang'): -> {
140                  'mspang': { 'cn': 'Michael Spang', ... },
141                  ...
142              ]
143     """
144
145     return ldap_connection.member_search_name(name)
146
147
148 def list_group(group):
149     """
150     Build a list of members in a group.
151
152     Parameters:
153         group - the group to match members against
154
155     Returns: a list of member dictionaries
156
157     Example: list_name('syscom'): -> {
158                  'mspang': { 'cn': 'Michael Spang', ... },
159                  ...
160              ]
161     """
162
163     members = group_members(group)
164     ret = {}
165     if members:
166         for member in members:
167             info = get(member)
168             if info:
169                 ret[member] = info
170     return ret
171
172
173 def list_positions():
174     """
175     Build a list of positions
176
177     Returns: a list of positions and who holds them
178
179     Example: list_positions(): -> {
180                  'president': { 'mspang': { 'cn': 'Michael Spang', ... } } ],
181                  ...
182              ]
183     """
184
185     ceo_ldap = ldap_connection.ldap
186     user_base = ldap_connection.user_base
187
188     members = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE, '(position=*)')
189     positions = {}
190     for (_, member) in members:
191         for position in member['position']:
192             if not position in positions:
193                 positions[position] = {}
194             positions[position][member['uid'][0]] = member
195     return positions
196
197 def set_position(position, members):
198     """
199     Sets a position
200
201     Parameters:
202         position - the position to set
203         members - an array of members that hold the position
204
205     Example: set_position('president', ['dtbartle'])
206     """
207
208     ceo_ldap = ldap_connection.ldap
209     user_base = ldap_connection.user_base
210     escape = ldap_connection.escape
211
212     res = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE,
213         '(&(objectClass=member)(position=%s))' % escape(position))
214     old = set([ member['uid'][0] for (_, member) in res ])
215     new = set(members)
216     mods = {
217         'del': set(old) - set(new),
218         'add': set(new) - set(old),
219     }
220     if len(mods['del']) == 0 and len(mods['add']) == 0:
221         return
222
223     for action in ['del', 'add']:
224         for userid in mods[action]:
225             dn = 'uid=%s,%s' % (escape(userid), user_base)
226             entry1 = {'position' : [position]}
227             entry2 = {} #{'position' : []}
228             entry = ()
229             if action == 'del':
230                 entry = (entry1, entry2)
231             elif action == 'add':
232                 entry = (entry2, entry1)
233             mlist = ldap_connection.make_modlist(entry[0], entry[1])
234             ceo_ldap.modify_s(dn, mlist)
235
236
237 def change_group_member(action, group, userid):
238
239     ceo_ldap = ldap_connection.ldap
240     user_base = ldap_connection.user_base
241     group_base = ldap_connection.group_base
242     escape = ldap_connection.escape
243
244     user_dn = 'uid=%s,%s' % (escape(userid), user_base)
245     group_dn = 'cn=%s,%s' % (escape(group), group_base)
246     entry1 = {'uniqueMember' : []}
247     entry2 = {'uniqueMember' : [user_dn]}
248     entry = []
249     if action == 'add' or action == 'insert':
250         entry = (entry1, entry2)
251     elif action == 'remove' or action == 'delete':
252         entry = (entry2, entry1)
253     else:
254         raise InvalidArgument("action", action, "invalid action")
255     mlist = ldap_connection.make_modlist(entry[0], entry[1])
256     ceo_ldap.modify_s(group_dn, mlist)
257
258
259 ### Terms ###
260
261 def register(userid, term_list):
262     """
263     Registers a member for one or more terms.
264
265     Parameters:
266         userid  - the member's username
267         term_list - the term to register for, or a list of terms
268
269     Exceptions:
270         InvalidTerm - if a term is malformed
271
272     Example: register(3349, "w2007")
273
274     Example: register(3349, ["w2007", "s2007"])
275     """
276
277     if type(term_list) in (str, unicode):
278         term_list = [ term_list ]
279
280     ldap_member = ldap_connection.member_lookup(userid)
281     if ldap_member and 'term' not in ldap_member:
282         ldap_member['term'] = []
283
284     if not ldap_member:
285         raise NoSuchMember(userid)
286
287     for term in term_list:
288
289         # check term syntax
290         if not re.match('^[wsf][0-9]{4}$', term):
291             raise InvalidTerm(term)
292
293         # add the term to the directory
294         ldap_member['term'].append(term)
295
296     ldap_connection.user_modify(userid, ldap_member)
297
298
299 def registered(userid, term):
300     """
301     Determines whether a member is registered
302     for a term.
303
304     Parameters:
305         userid   - the member's username
306         term     - the term to check
307
308     Returns: whether the member is registered
309
310     Example: registered("mspang", "f2006") -> True
311     """
312
313     member = ldap_connection.member_lookup(userid)
314     return 'term' in member and term in member['term']
315
316
317 def group_members(group):
318
319     """
320     Returns a list of group members
321     """
322
323     group = ldap_connection.group_lookup(group)
324     if group:
325         if 'uniqueMember' in group:
326             r = re.compile('^uid=([^,]*)')
327             return map(lambda x: r.match(x).group(1), group['uniqueMember'])
328         elif 'memberUid' in group:
329             return group['memberUid']
330         else:
331             return []
332     else:
333         return []