Add PersonTags as well. Performed a simple test but not extensive regression
[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         'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
61         }
62     related_fields = {
63         'roles': [Mixed(Parameter(int, "Role identifier"),
64                         Parameter(str, "Role name"))],
65         'sites': [Mixed(Parameter(int, "Site identifier"),
66                         Parameter(str, "Site name"))],
67         'keys': [Mixed(Parameter(int, "Key identifier"),
68                        Filter(Key.fields))],
69         'slices': [Mixed(Parameter(int, "Slice identifier"),
70                          Parameter(str, "Slice name"))]
71         }       
72     view_tags_name = "view_person_tags"
73     # tags are used by the Add/Get/Update methods to expose tags
74     # this is initialized here and updated by the accessors factory
75     tags = { }
76
77     def validate_email(self, email):
78         """
79         Validate email address. Stolen from Mailman.
80         """
81
82         invalid_email = PLCInvalidArgument("Invalid e-mail address")
83         email_badchars = r'[][()<>|;^,\200-\377]'
84
85         # Pretty minimal, cheesy check.  We could do better...
86         if not email or email.count(' ') > 0:
87             raise invalid_email
88         if re.search(email_badchars, email) or email[0] == '-':
89             raise invalid_email
90
91         email = email.lower()
92         at_sign = email.find('@')
93         if at_sign < 1:
94             raise invalid_email
95         user = email[:at_sign]
96         rest = email[at_sign+1:]
97         domain = rest.split('.')
98
99         # This means local, unqualified addresses, are not allowed
100         if not domain:
101             raise invalid_email
102         if len(domain) < 2:
103             raise invalid_email
104
105         # check only against users on the same peer  
106         if 'peer_id' in self:
107             namespace_peer_id = self['peer_id']
108         else:
109             namespace_peer_id = None
110          
111         conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id}) 
112         
113         for person in conflicts:
114             if 'person_id' not in self or self['person_id'] != person['person_id']:
115                 raise PLCInvalidArgument, "E-mail address already in use"
116
117         return email
118
119     def validate_password(self, password):
120         """
121         Encrypt password if necessary before committing to the
122         database.
123         """
124
125         magic = "$1$"
126
127         if len(password) > len(magic) and \
128            password[0:len(magic)] == magic:
129             return password
130         else:
131             # Generate a somewhat unique 8 character salt string
132             salt = str(time.time()) + str(Random().random())
133             salt = md5.md5(salt).hexdigest()[:8] 
134             return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
135
136     validate_date_created = Row.validate_timestamp
137     validate_last_updated = Row.validate_timestamp
138     validate_verification_expires = Row.validate_timestamp
139
140     def can_update(self, person):
141         """
142         Returns true if we can update the specified person. We can
143         update a person if:
144
145         1. We are the person.
146         2. We are an admin.
147         3. We are a PI and the person is a user or tech or at
148            one of our sites.
149         """
150
151         assert isinstance(person, Person)
152
153         if self['person_id'] == person['person_id']:
154             return True
155
156         if 'admin' in self['roles']:
157             return True
158
159         if 'pi' in self['roles']:
160             if set(self['site_ids']).intersection(person['site_ids']):
161                 # Can update person is neither a PI or ADMIN
162                 return (not (('pi' in person['roles']) or ('admin' in person['roles'])))
163
164         return False
165
166     def can_view(self, person):
167         """
168         Returns true if we can view the specified person. We can
169         view a person if:
170
171         1. We are the person.
172         2. We are an admin.
173         3. We are a PI and the person is at one of our sites.
174         """
175
176         assert isinstance(person, Person)
177
178         if self.can_update(person):
179             return True
180
181         if 'pi' in self['roles']:
182             if set(self['site_ids']).intersection(person['site_ids']):
183                 # Can view people with equal or higher role IDs
184                 return 'admin' not in person['roles']
185
186         return False
187
188     add_role = Row.add_object(Role, 'person_role')
189     remove_role = Row.remove_object(Role, 'person_role')
190
191     add_key = Row.add_object(Key, 'person_key')
192     remove_key = Row.remove_object(Key, 'person_key')
193
194     def set_primary_site(self, site, commit = True):
195         """
196         Set the primary site for an existing user.
197         """
198
199         assert 'person_id' in self
200         assert 'site_id' in site
201
202         person_id = self['person_id']
203         site_id = site['site_id']
204         self.api.db.do("UPDATE person_site SET is_primary = False" \
205                        " WHERE person_id = %(person_id)d",
206                        locals())
207         self.api.db.do("UPDATE person_site SET is_primary = True" \
208                        " WHERE person_id = %(person_id)d" \
209                        " AND site_id = %(site_id)d",
210                        locals())
211
212         if commit:
213             self.api.db.commit()
214
215         assert 'site_ids' in self
216         assert site_id in self['site_ids']
217
218         # Make sure that the primary site is first in the list
219         self['site_ids'].remove(site_id)
220         self['site_ids'].insert(0, site_id)
221
222     def update_last_updated(self, commit = True):
223         """
224         Update last_updated field with current time
225         """
226         
227         assert 'person_id' in self
228         assert self.table_name
229         
230         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
231                        " where person_id = %d" % (self['person_id']) )
232         self.sync(commit)
233
234     def associate_roles(self, auth, field, value):
235         """
236         Adds roles found in value list to this person (using AddRoleToPerson).
237         Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
238         """
239         
240         assert 'role_ids' in self
241         assert 'person_id' in self
242         assert isinstance(value, list)
243         
244         (role_ids, role_names) = self.separate_types(value)[0:2]
245         
246         # Translate roles into role_ids
247         if role_names:
248             roles = Roles(self.api, role_names).dict('role_id')
249             role_ids += roles.keys()
250         
251         # Add new ids, remove stale ids
252         if self['role_ids'] != role_ids:
253             from PLC.Methods.AddRoleToPerson import AddRoleToPerson
254             from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
255             new_roles = set(role_ids).difference(self['role_ids'])
256             stale_roles = set(self['role_ids']).difference(role_ids)
257
258             for new_role in new_roles:
259                 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
260             for stale_role in stale_roles:
261                 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
262
263
264     def associate_sites(self, auth, field, value):
265         """
266         Adds person to sites found in value list (using AddPersonToSite).
267         Deletes person from site not found in value list (using DeletePersonFromSite).
268         """
269
270         from PLC.Sites import Sites
271
272         assert 'site_ids' in self
273         assert 'person_id' in self
274         assert isinstance(value, list)
275
276         (site_ids, site_names) = self.separate_types(value)[0:2]
277
278         # Translate roles into role_ids
279         if site_names:
280             sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
281             site_ids += sites.keys()
282
283         # Add new ids, remove stale ids
284         if self['site_ids'] != site_ids:
285             from PLC.Methods.AddPersonToSite import AddPersonToSite
286             from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
287             new_sites = set(site_ids).difference(self['site_ids'])
288             stale_sites = set(self['site_ids']).difference(site_ids)
289
290             for new_site in new_sites:
291                 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
292             for stale_site in stale_sites:
293                 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
294
295
296     def associate_keys(self, auth, field, value):
297         """
298         Deletes key_ids not found in value list (using DeleteKey).
299         Adds key if key_fields w/o key_id is found (using AddPersonKey).
300         Updates key if key_fields w/ key_id is found (using UpdateKey).
301         """
302         assert 'key_ids' in self
303         assert 'person_id' in self
304         assert isinstance(value, list)
305         
306         (key_ids, blank, keys) = self.separate_types(value)
307         
308         if self['key_ids'] != key_ids:
309             from PLC.Methods.DeleteKey import DeleteKey
310             stale_keys = set(self['key_ids']).difference(key_ids)
311         
312             for stale_key in stale_keys:
313                 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key) 
314
315         if keys:
316             from PLC.Methods.AddPersonKey import AddPersonKey
317             from PLC.Methods.UpdateKey import UpdateKey         
318             updated_keys = filter(lambda key: 'key_id' in key, keys)
319             added_keys = filter(lambda key: 'key_id' not in key, keys)
320                 
321             for key in added_keys:
322                 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
323             for key in updated_keys:
324                 key_id = key.pop('key_id')
325                 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
326                   
327         
328     def associate_slices(self, auth, field, value):
329         """
330         Adds person to slices found in value list (using AddPersonToSlice).
331         Deletes person from slices found in value list (using DeletePersonFromSlice).
332         """
333
334         from PLC.Slices import Slices
335
336         assert 'slice_ids' in self
337         assert 'person_id' in self
338         assert isinstance(value, list)
339
340         (slice_ids, slice_names) = self.separate_types(value)[0:2]
341
342         # Translate roles into role_ids
343         if slice_names:
344             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
345             slice_ids += slices.keys()
346
347         # Add new ids, remove stale ids
348         if self['slice_ids'] != slice_ids:
349             from PLC.Methods.AddPersonToSlice import AddPersonToSlice
350             from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
351             new_slices = set(slice_ids).difference(self['slice_ids'])
352             stale_slices = set(self['slice_ids']).difference(slice_ids)
353
354             for new_slice in new_slices:
355                 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
356             for stale_slice in stale_slices:
357                 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
358     
359
360     def delete(self, commit = True):
361         """
362         Delete existing user.
363         """
364
365         # Delete all keys
366         keys = Keys(self.api, self['key_ids'])
367         for key in keys:
368             key.delete(commit = False)
369
370         # Clean up miscellaneous join tables
371         for table in self.join_tables:
372             self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
373                            (table, self['person_id']))
374
375         # Mark as deleted
376         self['deleted'] = True
377         self.sync(commit)
378
379 class Persons(Table):
380     """
381     Representation of row(s) from the persons table in the
382     database.
383     """
384
385     def __init__(self, api, person_filter = None, columns = None):
386         Table.__init__(self, api, Person, columns)
387
388         view = "view_persons"
389         for tagname in self.tag_columns:
390             view= "%s left join %s using (%s)"%(view,Person.tagvalue_view_name(tagname),
391                                                 Person.primary_key)
392             
393         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
394             (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
395
396         if person_filter is not None:
397             if isinstance(person_filter, (list, tuple, set)):
398                 # Separate the list into integers and strings
399                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
400                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
401                 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
402                 sql += " AND (%s) %s" % person_filter.sql(api, "OR")
403             elif isinstance(person_filter, dict):
404                 person_filter = Filter(Person.fields, person_filter)
405                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
406             elif isinstance (person_filter, StringTypes):
407                 person_filter = Filter(Person.fields, {'email':[person_filter]})
408                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
409             elif isinstance (person_filter, int):
410                 person_filter = Filter(Person.fields, {'person_id':[person_filter]})
411                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
412             else:
413                 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
414
415         self.selectall(sql)