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