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
9 from hashlib import md5
13 from random import Random
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
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().
32 table_name = 'persons'
33 primary_key = 'person_id'
34 join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
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"),
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"),
66 'slices': [Mixed(Parameter(int, "Slice identifier"),
67 Parameter(str, "Slice name"))]
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
74 def validate_email(self, email):
76 Validate email address. Stolen from Mailman.
79 invalid_email = PLCInvalidArgument("Invalid e-mail address %s"%email)
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):
88 # check only against users on the same peer
90 namespace_peer_id = self['peer_id']
92 namespace_peer_id = None
94 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
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")
102 def validate_password(self, password):
104 Encrypt password if necessary before committing to the
110 if len(password) > len(magic) and \
111 password[0:len(magic)] == magic:
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 + "$")
119 validate_date_created = Row.validate_timestamp
120 validate_last_updated = Row.validate_timestamp
121 validate_verification_expires = Row.validate_timestamp
123 def can_update(self, person):
125 Returns true if we can update the specified person. We can
128 1. We are the person.
130 3. We are a PI and the person is a user or tech or at
134 assert isinstance(person, Person)
136 if self['person_id'] == person['person_id']:
139 if 'admin' in self['roles']:
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']))
149 def can_view(self, person):
151 Returns true if we can view the specified person. We can
154 1. We are the person.
156 3. We are a PI or Tech and the person is at one of our sites.
159 assert isinstance(person, Person)
161 if self.can_update(person):
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']):
171 add_role = Row.add_object(Role, 'person_role')
172 remove_role = Row.remove_object(Role, 'person_role')
174 add_key = Row.add_object(Key, 'person_key')
175 remove_key = Row.remove_object(Key, 'person_key')
177 def set_primary_site(self, site, commit = True):
179 Set the primary site for an existing user.
182 assert 'person_id' in self
183 assert 'site_id' in site
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",
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",
198 assert 'site_ids' in self
199 assert site_id in self['site_ids']
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)
205 def update_last_updated(self, commit = True):
207 Update last_updated field with current time
210 assert 'person_id' in self
211 assert self.table_name
213 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
214 " where person_id = %d" % (self['person_id']) )
217 def associate_roles(self, auth, field, value):
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).
223 assert 'role_ids' in self
224 assert 'person_id' in self
225 assert isinstance(value, list)
227 (role_ids, role_names) = self.separate_types(value)[0:2]
229 # Translate roles into role_ids
231 roles = Roles(self.api, role_names).dict('role_id')
232 role_ids += list(roles.keys())
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)
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'])
247 def associate_sites(self, auth, field, value):
249 Adds person to sites found in value list (using AddPersonToSite).
250 Deletes person from site not found in value list (using DeletePersonFromSite).
253 from PLC.Sites import Sites
255 assert 'site_ids' in self
256 assert 'person_id' in self
257 assert isinstance(value, list)
259 (site_ids, site_names) = self.separate_types(value)[0:2]
261 # Translate roles into role_ids
263 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
264 site_ids += list(sites.keys())
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)
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)
279 def associate_keys(self, auth, field, value):
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).
285 assert 'key_ids' in self
286 assert 'person_id' in self
287 assert isinstance(value, list)
289 (key_ids, blank, keys) = self.separate_types(value)
291 if self['key_ids'] != key_ids:
292 from PLC.Methods.DeleteKey import DeleteKey
293 stale_keys = set(self['key_ids']).difference(key_ids)
295 for stale_key in stale_keys:
296 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
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]
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)
311 def associate_slices(self, auth, field, value):
313 Adds person to slices found in value list (using AddPersonToSlice).
314 Deletes person from slices found in value list (using DeletePersonFromSlice).
317 from PLC.Slices import Slices
319 assert 'slice_ids' in self
320 assert 'person_id' in self
321 assert isinstance(value, list)
323 (slice_ids, slice_names) = self.separate_types(value)[0:2]
325 # Translate roles into role_ids
327 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
328 slice_ids += list(slices.keys())
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)
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)
343 def delete(self, commit = True):
345 Delete existing user.
349 keys = Keys(self.api, self['key_ids'])
351 key.delete(commit = False)
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']))
359 self['deleted'] = True
361 # delete will fail if timestamp fields aren't validated, so lets remove them
362 for field in ['verification_expires', 'date_created', 'last_updated']:
366 # don't validate, so duplicates can be consistently removed
367 self.sync(commit, validate=False)
369 class Persons(Table):
371 Representation of row(s) from the persons table in the
375 def __init__(self, api, person_filter = None, columns = None):
376 Table.__init__(self, api, Person, columns)
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),
383 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
384 (", ".join(list(self.columns.keys())+list(self.tag_columns.keys())), view)
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")
404 raise PLCInvalidArgument("Wrong person filter %r"%person_filter)