6feb270d7e45387c185a0b6a7e9e470eddf6063f
[plcapi.git] / PLC / Persons.py
1 #
2 # Functions for interacting with the persons table in the database
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id$
8 # $URL$
9 #
10
11 from types import StringTypes
12 from datetime import datetime
13 import md5
14 import time
15 from random import Random
16 import re
17 import crypt
18
19 from PLC.Faults import *
20 from PLC.Debug import log
21 from PLC.Parameter import Parameter, Mixed
22 from PLC.Filter import Filter
23 from PLC.Table import Row, Table
24 from PLC.Roles import Role, Roles
25 from PLC.Keys import Key, Keys
26 from PLC.Messages import Message, Messages
27
28 class Person(Row):
29     """
30     Representation of a row in the persons table. To use, optionally
31     instantiate with a dict of values. Update as you would a
32     dict. Commit to the database with sync().
33     """
34
35     table_name = 'persons'
36     primary_key = 'person_id'
37     join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
38     fields = {
39         'person_id': Parameter(int, "User identifier"),
40         'first_name': Parameter(str, "Given name", max = 128),
41         'last_name': Parameter(str, "Surname", max = 128),
42         'title': Parameter(str, "Title", max = 128, nullok = True),
43         'email': Parameter(str, "Primary e-mail address", max = 254),
44         'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
45         'url': Parameter(str, "Home page", max = 254, nullok = True),
46         'bio': Parameter(str, "Biography", max = 254, nullok = True),
47         'enabled': Parameter(bool, "Has been enabled"),
48         'password': Parameter(str, "Account password in crypt() form", max = 254),
49         'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
50         'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
51         'last_updated': Parameter(int, "Date and time of last update", ro = True),
52         'date_created': Parameter(int, "Date and time when account was created", ro = True),
53         'role_ids': Parameter([int], "List of role identifiers"),
54         'roles': Parameter([str], "List of roles"),
55         'site_ids': Parameter([int], "List of site identifiers"),
56         'key_ids': Parameter([int], "List of key identifiers"),
57         'slice_ids': Parameter([int], "List of slice identifiers"),
58         'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
59         'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
60         }
61     related_fields = {
62         'roles': [Mixed(Parameter(int, "Role identifier"),
63                         Parameter(str, "Role name"))],
64         'sites': [Mixed(Parameter(int, "Site identifier"),
65                         Parameter(str, "Site name"))],
66         'keys': [Mixed(Parameter(int, "Key identifier"),
67                        Filter(Key.fields))],
68         'slices': [Mixed(Parameter(int, "Slice identifier"),
69                          Parameter(str, "Slice name"))]
70         }       
71
72     def validate_email(self, email):
73         """
74         Validate email address. Stolen from Mailman.
75         """
76
77         invalid_email = PLCInvalidArgument("Invalid e-mail address")
78         email_badchars = r'[][()<>|;^,\200-\377]'
79
80         # Pretty minimal, cheesy check.  We could do better...
81         if not email or email.count(' ') > 0:
82             raise invalid_email
83         if re.search(email_badchars, email) or email[0] == '-':
84             raise invalid_email
85
86         email = email.lower()
87         at_sign = email.find('@')
88         if at_sign < 1:
89             raise invalid_email
90         user = email[:at_sign]
91         rest = email[at_sign+1:]
92         domain = rest.split('.')
93
94         # This means local, unqualified addresses, are not allowed
95         if not domain:
96             raise invalid_email
97         if len(domain) < 2:
98             raise invalid_email
99
100         # check only against users on the same peer  
101         if 'peer_id' in self:
102             namespace_peer_id = self['peer_id']
103         else:
104             namespace_peer_id = None
105          
106         conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id}) 
107         
108         for person in conflicts:
109             if 'person_id' not in self or self['person_id'] != person['person_id']:
110                 raise PLCInvalidArgument, "E-mail address already in use"
111
112         return email
113
114     def validate_password(self, password):
115         """
116         Encrypt password if necessary before committing to the
117         database.
118         """
119
120         magic = "$1$"
121
122         if len(password) > len(magic) and \
123            password[0:len(magic)] == magic:
124             return password
125         else:
126             # Generate a somewhat unique 8 character salt string
127             salt = str(time.time()) + str(Random().random())
128             salt = md5.md5(salt).hexdigest()[:8] 
129             return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
130
131     validate_date_created = Row.validate_timestamp
132     validate_last_updated = Row.validate_timestamp
133     validate_verification_expires = Row.validate_timestamp
134
135     def can_update(self, person):
136         """
137         Returns true if we can update the specified person. We can
138         update a person if:
139
140         1. We are the person.
141         2. We are an admin.
142         3. We are a PI and the person is a user or tech or at
143            one of our sites.
144         """
145
146         assert isinstance(person, Person)
147
148         if self['person_id'] == person['person_id']:
149             return True
150
151         if 'admin' in self['roles']:
152             return True
153
154         if 'pi' in self['roles']:
155             if set(self['site_ids']).intersection(person['site_ids']):
156                 # Can update person is neither a PI or ADMIN
157                 return (not (('pi' in person['roles']) or ('admin' in person['roles'])))
158
159         return False
160
161     def can_view(self, person):
162         """
163         Returns true if we can view the specified person. We can
164         view a person if:
165
166         1. We are the person.
167         2. We are an admin.
168         3. We are a PI and the person is at one of our sites.
169         """
170
171         assert isinstance(person, Person)
172
173         if self.can_update(person):
174             return True
175
176         if 'pi' in self['roles']:
177             if set(self['site_ids']).intersection(person['site_ids']):
178                 # Can view people with equal or higher role IDs
179                 return 'admin' not in person['roles']
180
181         return False
182
183     add_role = Row.add_object(Role, 'person_role')
184     remove_role = Row.remove_object(Role, 'person_role')
185
186     add_key = Row.add_object(Key, 'person_key')
187     remove_key = Row.remove_object(Key, 'person_key')
188
189     def set_primary_site(self, site, commit = True):
190         """
191         Set the primary site for an existing user.
192         """
193
194         assert 'person_id' in self
195         assert 'site_id' in site
196
197         person_id = self['person_id']
198         site_id = site['site_id']
199         self.api.db.do("UPDATE person_site SET is_primary = False" \
200                        " WHERE person_id = %(person_id)d",
201                        locals())
202         self.api.db.do("UPDATE person_site SET is_primary = True" \
203                        " WHERE person_id = %(person_id)d" \
204                        " AND site_id = %(site_id)d",
205                        locals())
206
207         if commit:
208             self.api.db.commit()
209
210         assert 'site_ids' in self
211         assert site_id in self['site_ids']
212
213         # Make sure that the primary site is first in the list
214         self['site_ids'].remove(site_id)
215         self['site_ids'].insert(0, site_id)
216
217     def update_last_updated(self, commit = True):
218         """
219         Update last_updated field with current time
220         """
221         
222         assert 'person_id' in self
223         assert self.table_name
224         
225         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
226                        " where person_id = %d" % (self['person_id']) )
227         self.sync(commit)
228
229     def associate_roles(self, auth, field, value):
230         """
231         Adds roles found in value list to this person (using AddRoleToPerson).
232         Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
233         """
234         
235         assert 'role_ids' in self
236         assert 'person_id' in self
237         assert isinstance(value, list)
238         
239         (role_ids, role_names) = self.separate_types(value)[0:2]
240         
241         # Translate roles into role_ids
242         if role_names:
243             roles = Roles(self.api, role_names).dict('role_id')
244             role_ids += roles.keys()
245         
246         # Add new ids, remove stale ids
247         if self['role_ids'] != role_ids:
248             from PLC.Methods.AddRoleToPerson import AddRoleToPerson
249             from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
250             new_roles = set(role_ids).difference(self['role_ids'])
251             stale_roles = set(self['role_ids']).difference(role_ids)
252
253             for new_role in new_roles:
254                 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
255             for stale_role in stale_roles:
256                 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
257
258
259     def associate_sites(self, auth, field, value):
260         """
261         Adds person to sites found in value list (using AddPersonToSite).
262         Deletes person from site not found in value list (using DeletePersonFromSite).
263         """
264
265         from PLC.Sites import Sites
266
267         assert 'site_ids' in self
268         assert 'person_id' in self
269         assert isinstance(value, list)
270
271         (site_ids, site_names) = self.separate_types(value)[0:2]
272
273         # Translate roles into role_ids
274         if site_names:
275             sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
276             site_ids += sites.keys()
277
278         # Add new ids, remove stale ids
279         if self['site_ids'] != site_ids:
280             from PLC.Methods.AddPersonToSite import AddPersonToSite
281             from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
282             new_sites = set(site_ids).difference(self['site_ids'])
283             stale_sites = set(self['site_ids']).difference(site_ids)
284
285             for new_site in new_sites:
286                 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
287             for stale_site in stale_sites:
288                 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
289
290
291     def associate_keys(self, auth, field, value):
292         """
293         Deletes key_ids not found in value list (using DeleteKey).
294         Adds key if key_fields w/o key_id is found (using AddPersonKey).
295         Updates key if key_fields w/ key_id is found (using UpdateKey).
296         """
297         assert 'key_ids' in self
298         assert 'person_id' in self
299         assert isinstance(value, list)
300         
301         (key_ids, blank, keys) = self.separate_types(value)
302         
303         if self['key_ids'] != key_ids:
304             from PLC.Methods.DeleteKey import DeleteKey
305             stale_keys = set(self['key_ids']).difference(key_ids)
306         
307             for stale_key in stale_keys:
308                 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key) 
309
310         if keys:
311             from PLC.Methods.AddPersonKey import AddPersonKey
312             from PLC.Methods.UpdateKey import UpdateKey         
313             updated_keys = filter(lambda key: 'key_id' in key, keys)
314             added_keys = filter(lambda key: 'key_id' not in key, keys)
315                 
316             for key in added_keys:
317                 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
318             for key in updated_keys:
319                 key_id = key.pop('key_id')
320                 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
321                   
322         
323     def associate_slices(self, auth, field, value):
324         """
325         Adds person to slices found in value list (using AddPersonToSlice).
326         Deletes person from slices found in value list (using DeletePersonFromSlice).
327         """
328
329         from PLC.Slices import Slices
330
331         assert 'slice_ids' in self
332         assert 'person_id' in self
333         assert isinstance(value, list)
334
335         (slice_ids, slice_names) = self.separate_types(value)[0:2]
336
337         # Translate roles into role_ids
338         if slice_names:
339             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
340             slice_ids += slices.keys()
341
342         # Add new ids, remove stale ids
343         if self['slice_ids'] != slice_ids:
344             from PLC.Methods.AddPersonToSlice import AddPersonToSlice
345             from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
346             new_slices = set(slice_ids).difference(self['slice_ids'])
347             stale_slices = set(self['slice_ids']).difference(slice_ids)
348
349             for new_slice in new_slices:
350                 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
351             for stale_slice in stale_slices:
352                 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
353     
354
355     def delete(self, commit = True):
356         """
357         Delete existing user.
358         """
359
360         # Delete all keys
361         keys = Keys(self.api, self['key_ids'])
362         for key in keys:
363             key.delete(commit = False)
364
365         # Clean up miscellaneous join tables
366         for table in self.join_tables:
367             self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
368                            (table, self['person_id']))
369
370         # Mark as deleted
371         self['deleted'] = True
372         self.sync(commit)
373
374 class Persons(Table):
375     """
376     Representation of row(s) from the persons table in the
377     database.
378     """
379
380     def __init__(self, api, person_filter = None, columns = None):
381         Table.__init__(self, api, Person, columns)
382
383         sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
384               ", ".join(self.columns)
385
386         if person_filter is not None:
387             if isinstance(person_filter, (list, tuple, set)):
388                 # Separate the list into integers and strings
389                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
390                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
391                 node_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
392                 sql += " AND (%s) %s" % person_filter.sql(api, "OR")
393             elif isinstance(person_filter, dict):
394                 person_filter = Filter(Person.fields, person_filter)
395                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
396             elif isinstance (person_filter, StringTypes):
397                 person_filter = Filter(Person.fields, {'email':[person_filter]})
398                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
399             elif isinstance (person_filter, int):
400                 person_filter = Filter(Person.fields, {'person_id':[person_filter]})
401                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
402             else:
403                 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
404
405         self.selectall(sql)