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
7 # $Id: Persons.py,v 1.26 2007/01/05 15:56:16 tmack Exp $
10 from types import StringTypes
11 from datetime import datetime
14 from random import Random
18 from PLC.Faults import *
19 from PLC.Parameter import Parameter
20 from PLC.Filter import Filter
21 from PLC.Table import Row, Table
22 from PLC.Keys import Key, Keys
23 from PLC.Messages import Message, Messages
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().
33 table_name = 'persons'
34 primary_key = 'person_id'
35 join_tables = ['person_role', 'person_site', 'slice_person', 'person_session']
37 'person_id': Parameter(int, "Account 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),
48 'verification_expires': Parameter(str, "Date/Time when verification_key expires", max = 254),
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 at which this slice was created", nullok = True),
61 foreign_fields = ['first_name', 'last_name', 'title', 'email', 'phone', 'url',
62 'bio', 'enabled', 'password', ]
63 # forget about these ones, they are read-only anyway
64 # handling them causes Cache to re-sync all over again
65 # 'last_updated', 'date_created'
67 {'field' : 'key_ids', 'class': 'Key', 'table' : 'person_key' } ,
68 {'field' : 'site_ids', 'class': 'Site', 'table' : 'person_site'},
69 # xxx this is not handled by Cache yet
70 # 'role_ids': Parameter([int], "List of role identifiers"),
73 def validate_email(self, email):
75 Validate email address. Stolen from Mailman.
78 invalid_email = PLCInvalidArgument("Invalid e-mail address")
79 email_badchars = r'[][()<>|;^,\200-\377]'
81 # Pretty minimal, cheesy check. We could do better...
82 if not email or email.count(' ') > 0:
84 if re.search(email_badchars, email) or email[0] == '-':
88 at_sign = email.find('@')
91 user = email[:at_sign]
92 rest = email[at_sign+1:]
93 domain = rest.split('.')
95 # This means local, unqualified addresses, are no allowed
101 conflicts = Persons(self.api, [email])
102 for person in conflicts:
103 if 'person_id' not in self or self['person_id'] != person['person_id']:
104 raise PLCInvalidArgument, "E-mail address already in use"
108 def validate_password(self, password):
110 Encrypt password if necessary before committing to the
116 if len(password) > len(magic) and \
117 password[0:len(magic)] == magic:
120 # Generate a somewhat unique 8 character salt string
121 salt = str(time.time()) + str(Random().random())
122 salt = md5.md5(salt).hexdigest()[:8]
123 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
126 # verification_expires in the DB but not exposed here
127 def validate_date_created (self, timestamp):
128 return self.validate_timestamp (timestamp)
129 def validate_last_updated (self, timestamp):
130 return self.validate_timestamp (timestamp)
132 def can_update(self, person):
134 Returns true if we can update the specified person. We can
137 1. We are the person.
139 3. We are a PI and the person is a user or tech or at
143 assert isinstance(person, Person)
145 if self['person_id'] == person['person_id']:
148 if 'admin' in self['roles']:
151 if 'pi' in self['roles']:
152 if set(self['site_ids']).intersection(person['site_ids']):
153 # Can update people with higher role IDs
154 return min(self['role_ids']) < min(person['role_ids'])
158 def can_view(self, person):
160 Returns true if we can view the specified person. We can
163 1. We are the person.
165 3. We are a PI and the person is at one of our sites.
168 assert isinstance(person, Person)
170 if self.can_update(person):
173 if 'pi' in self['roles']:
174 if set(self['site_ids']).intersection(person['site_ids']):
175 # Can view people with equal or higher role IDs
176 return min(self['role_ids']) <= min(person['role_ids'])
180 def add_role(self, role_id, commit = True):
182 Add role to existing account.
185 assert 'person_id' in self
187 person_id = self['person_id']
189 if role_id not in self['role_ids']:
190 self.api.db.do("INSERT INTO person_role (person_id, role_id)" \
191 " VALUES(%(person_id)d, %(role_id)d)",
197 self['role_ids'].append(role_id)
199 def remove_role(self, role_id, commit = True):
201 Remove role from existing account.
204 assert 'person_id' in self
206 person_id = self['person_id']
208 if role_id in self['role_ids']:
209 self.api.db.do("DELETE FROM person_role" \
210 " WHERE person_id = %(person_id)d" \
211 " AND role_id = %(role_id)d",
217 self['role_ids'].remove(role_id)
219 def add_key(self, key, commit = True):
221 Add key to existing account.
224 assert 'person_id' in self
225 assert isinstance(key, Key)
226 assert 'key_id' in key
228 person_id = self['person_id']
229 key_id = key['key_id']
231 if key_id not in self['key_ids']:
232 self.api.db.do("INSERT INTO person_key (person_id, key_id)" \
233 " VALUES(%(person_id)d, %(key_id)d)",
239 self['key_ids'].append(key_id)
241 def remove_key(self, key, commit = True):
243 Remove key from existing account.
246 assert 'person_id' in self
247 assert isinstance(key, Key)
248 assert 'key_id' in key
250 person_id = self['person_id']
251 key_id = key['key_id']
253 if key_id in self['key_ids']:
254 self.api.db.do("DELETE FROM person_key" \
255 " WHERE person_id = %(person_id)d" \
256 " AND key_id = %(key_id)d",
262 self['key_ids'].remove(key_id)
264 def set_primary_site(self, site, commit = True):
266 Set the primary site for an existing account.
269 assert 'person_id' in self
270 assert isinstance(site, PLC.Sites.Site)
271 assert 'site_id' in site
273 person_id = self['person_id']
274 site_id = site['site_id']
275 self.api.db.do("UPDATE person_site SET is_primary = False" \
276 " WHERE person_id = %(person_id)d",
278 self.api.db.do("UPDATE person_site SET is_primary = True" \
279 " WHERE person_id = %(person_id)d" \
280 " AND site_id = %(site_id)d",
286 assert 'site_ids' in self
287 assert site_id in self['site_ids']
289 # Make sure that the primary site is first in the list
290 self['site_ids'].remove(site_id)
291 self['site_ids'].insert(0, site_id)
293 def send_initiate_password_reset_email(self):
294 # email user next step instructions
296 to_addr[self['email']] = "%s %s" % \
297 (self['first_name'], self['last_name'])
299 from_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
300 "%s %s" % ('Planetlab', 'Support')
303 messages = Messages(self.api, ['PASSWORD_RESET_INITIATE'])
305 raise PLCAPIError, "Email template not found"
307 message = messages[0]
308 subject = message['subject']
309 template = message['template'] % \
310 (self.api.config.PLC_WWW_HOST,
311 self['verification_key'], self['person_id'],
312 self.api.config.PLC_MAIL_SUPPORT_ADDRESS,
313 self.api.config.PLC_WWW_HOST)
315 self.api.mailer.mail(to_addr, None, from_addr, subject, template)
317 def send_account_registered_email(self, site):
322 from_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
323 "%s %s" % ('Planetlab', 'Support')
326 user_full_name = "%s %s" % (self['first_name'], self['last_name'])
327 to_addr[self['email']] = "%s" % user_full_name
329 # if the account had a admin role or a pi role, email support.
330 if set(['admin', 'pi']).intersection(self['roles']):
331 to_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
332 "%s %s" % ('Planetlab', 'Support')
335 site_persons = Persons(self.api, site['person_ids'])
336 for person in site_persons:
337 if 'pi' in person['roles'] and not person['email'] in to_addr.keys():
338 cc_addr[person['email']] = "%s %s" % \
339 (person['first_name'], person['last_name'])
342 messages = Messages(self.api, ['ACCOUNT_REGISTERED'])
344 raise PLCAPIError, "Email template not found"
346 message = messages[0]
347 subject = message['subject'] % (user_full_name, site['name'])
348 template = message['template'] % \
349 (user_full_name, site['name'], ", ".join(self['roles']),
350 self.api.config.PLC_WWW_HOST, self['person_id'],
351 self.api.config.PLC_MAIL_SUPPORT_ADDRESS,
352 self.api.config.PLC_WWW_HOST)
354 self.api.mailer.mail(to_addr, cc_addr, from_addr, subject, template)
356 def delete(self, commit = True):
358 Delete existing account.
362 keys = Keys(self.api, self['key_ids'])
364 key.delete(commit = False)
366 # Clean up miscellaneous join tables
367 for table in self.join_tables:
368 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
369 (table, self['person_id']))
372 self['deleted'] = True
375 class Persons(Table):
377 Representation of row(s) from the persons table in the
381 def __init__(self, api, person_filter = None, columns = None):
382 Table.__init__(self, api, Person, columns)
384 sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
385 ", ".join(self.columns)
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)" % person_filter.sql(api, "OR")
394 elif isinstance(person_filter, dict):
395 person_filter = Filter(Person.fields, person_filter)
396 sql += " AND (%s)" % person_filter.sql(api, "AND")