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