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