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