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