2 # Functions for interacting with the persons table in the database
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
8 from hashlib import md5
10 from random import Random
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
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().
29 table_name = 'persons'
30 primary_key = 'person_id'
31 join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
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"),
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"),
63 'slices': [Mixed(Parameter(int, "Slice identifier"),
64 Parameter(str, "Slice name"))]
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
71 def validate_email(self, email):
73 Validate email address. Stolen from Mailman.
76 invalid_email = PLCInvalidArgument("Invalid e-mail address %s"%email)
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):
85 # check only against users on the same peer
87 namespace_peer_id = self['peer_id']
89 namespace_peer_id = None
91 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
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")
99 def validate_password(self, password):
101 Encrypt password if necessary before committing to the
107 if len(password) > len(magic) and \
108 password[0:len(magic)] == magic:
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 + "$")
116 validate_date_created = Row.validate_timestamp
117 validate_last_updated = Row.validate_timestamp
118 validate_verification_expires = Row.validate_timestamp
120 def can_update(self, person):
122 Returns true if we can update the specified person. We can
125 1. We are the person.
127 3. We are a PI and the person is a user or tech or at
131 assert isinstance(person, Person)
133 if self['person_id'] == person['person_id']:
136 if 'admin' in self['roles']:
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']))
146 def can_view(self, person):
148 Returns true if we can view the specified person. We can
151 1. We are the person.
153 3. We are a PI or Tech and the person is at one of our sites.
156 assert isinstance(person, Person)
158 if self.can_update(person):
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']):
168 add_role = Row.add_object(Role, 'person_role')
169 remove_role = Row.remove_object(Role, 'person_role')
171 add_key = Row.add_object(Key, 'person_key')
172 remove_key = Row.remove_object(Key, 'person_key')
174 def set_primary_site(self, site, commit = True):
176 Set the primary site for an existing user.
179 assert 'person_id' in self
180 assert 'site_id' in site
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",
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",
195 assert 'site_ids' in self
196 assert site_id in self['site_ids']
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)
202 def update_last_updated(self, commit = True):
204 Update last_updated field with current time
207 assert 'person_id' in self
208 assert self.table_name
210 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
211 " where person_id = %d" % (self['person_id']) )
214 def associate_roles(self, auth, field, value):
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).
220 assert 'role_ids' in self
221 assert 'person_id' in self
222 assert isinstance(value, list)
224 (role_ids, role_names) = self.separate_types(value)[0:2]
226 # Translate roles into role_ids
228 roles = Roles(self.api, role_names).dict('role_id')
229 role_ids += list(roles.keys())
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)
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'])
244 def associate_sites(self, auth, field, value):
246 Adds person to sites found in value list (using AddPersonToSite).
247 Deletes person from site not found in value list (using DeletePersonFromSite).
250 from PLC.Sites import Sites
252 assert 'site_ids' in self
253 assert 'person_id' in self
254 assert isinstance(value, list)
256 (site_ids, site_names) = self.separate_types(value)[0:2]
258 # Translate roles into role_ids
260 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
261 site_ids += list(sites.keys())
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)
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)
276 def associate_keys(self, auth, field, value):
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).
282 assert 'key_ids' in self
283 assert 'person_id' in self
284 assert isinstance(value, list)
286 (key_ids, blank, keys) = self.separate_types(value)
288 if self['key_ids'] != key_ids:
289 from PLC.Methods.DeleteKey import DeleteKey
290 stale_keys = set(self['key_ids']).difference(key_ids)
292 for stale_key in stale_keys:
293 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
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]
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)
308 def associate_slices(self, auth, field, value):
310 Adds person to slices found in value list (using AddPersonToSlice).
311 Deletes person from slices found in value list (using DeletePersonFromSlice).
314 from PLC.Slices import Slices
316 assert 'slice_ids' in self
317 assert 'person_id' in self
318 assert isinstance(value, list)
320 (slice_ids, slice_names) = self.separate_types(value)[0:2]
322 # Translate roles into role_ids
324 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
325 slice_ids += list(slices.keys())
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)
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)
340 def delete(self, commit = True):
342 Delete existing user.
346 keys = Keys(self.api, self['key_ids'])
348 key.delete(commit = False)
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']))
356 self['deleted'] = True
358 # delete will fail if timestamp fields aren't validated, so lets remove them
359 for field in ['verification_expires', 'date_created', 'last_updated']:
363 # don't validate, so duplicates can be consistently removed
364 self.sync(commit, validate=False)
366 class Persons(Table):
368 Representation of row(s) from the persons table in the
372 def __init__(self, api, person_filter = None, columns = None):
373 Table.__init__(self, api, Person, columns)
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),
380 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
381 (", ".join(list(self.columns.keys())+list(self.tag_columns.keys())), view)
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")
401 raise PLCInvalidArgument("Wrong person filter %r"%person_filter)