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.29 2007/01/08 16:34:12 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.Debug import log
20 from PLC.Parameter import Parameter
21 from PLC.Filter import Filter
22 from PLC.Table import Row, Table
23 from PLC.Keys import Key, Keys
24 from PLC.Messages import Message, Messages
29 Representation of a row in the persons table. To use, optionally
30 instantiate with a dict of values. Update as you would a
31 dict. Commit to the database with sync().
34 table_name = 'persons'
35 primary_key = 'person_id'
36 join_tables = ['person_role', 'person_site', 'slice_person', 'person_session']
38 'person_id': Parameter(int, "Account identifier"),
39 'first_name': Parameter(str, "Given name", max = 128),
40 'last_name': Parameter(str, "Surname", max = 128),
41 'title': Parameter(str, "Title", max = 128, nullok = True),
42 'email': Parameter(str, "Primary e-mail address", max = 254),
43 'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
44 'url': Parameter(str, "Home page", max = 254, nullok = True),
45 'bio': Parameter(str, "Biography", max = 254, nullok = True),
46 'enabled': Parameter(bool, "Has been enabled"),
47 'password': Parameter(str, "Account password in crypt() form", max = 254),
48 'verification_key': Parameter(str, "Reset password key", max = 254),
49 'verification_expires': Parameter(str, "Date/Time when verification_key expires", max = 254),
50 'last_updated': Parameter(int, "Date and time of last update", ro = True),
51 'date_created': Parameter(int, "Date and time when account was created", ro = True),
52 'role_ids': Parameter([int], "List of role identifiers"),
53 'roles': Parameter([str], "List of roles"),
54 'site_ids': Parameter([int], "List of site identifiers"),
55 'key_ids': Parameter([int], "List of key identifiers"),
56 'slice_ids': Parameter([int], "List of slice identifiers"),
57 'peer_id': Parameter(int, "Peer at which this slice was created", nullok = True),
62 foreign_fields = ['first_name', 'last_name', 'title', 'email', 'phone', 'url',
63 'bio', 'enabled', 'password', ]
64 # forget about these ones, they are read-only anyway
65 # handling them causes Cache to re-sync all over again
66 # 'last_updated', 'date_created'
68 {'field' : 'key_ids', 'class': 'Key', 'table' : 'person_key' } ,
69 {'field' : 'site_ids', 'class': 'Site', 'table' : 'person_site'},
70 # xxx this is not handled by Cache yet
71 # 'role_ids': Parameter([int], "List of role identifiers"),
74 def validate_email(self, email):
76 Validate email address. Stolen from Mailman.
79 invalid_email = PLCInvalidArgument("Invalid e-mail address")
80 email_badchars = r'[][()<>|;^,\200-\377]'
82 # Pretty minimal, cheesy check. We could do better...
83 if not email or email.count(' ') > 0:
85 if re.search(email_badchars, email) or email[0] == '-':
89 at_sign = email.find('@')
92 user = email[:at_sign]
93 rest = email[at_sign+1:]
94 domain = rest.split('.')
96 # This means local, unqualified addresses, are no allowed
102 conflicts = Persons(self.api, [email])
103 for person in conflicts:
104 if 'person_id' not in self or self['person_id'] != person['person_id']:
105 raise PLCInvalidArgument, "E-mail address already in use"
109 def validate_password(self, password):
111 Encrypt password if necessary before committing to the
117 if len(password) > len(magic) and \
118 password[0:len(magic)] == magic:
121 # Generate a somewhat unique 8 character salt string
122 salt = str(time.time()) + str(Random().random())
123 salt = md5.md5(salt).hexdigest()[:8]
124 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
127 # verification_expires in the DB but not exposed here
128 def validate_date_created (self, timestamp):
129 return self.validate_timestamp (timestamp)
130 def validate_last_updated (self, timestamp):
131 return self.validate_timestamp (timestamp)
133 def can_update(self, person):
135 Returns true if we can update the specified person. We can
138 1. We are the person.
140 3. We are a PI and the person is a user or tech or at
144 assert isinstance(person, Person)
146 if self['person_id'] == person['person_id']:
149 if 'admin' in self['roles']:
152 if 'pi' in self['roles']:
153 if set(self['site_ids']).intersection(person['site_ids']):
154 # Can update people with higher role IDs
155 return min(self['role_ids']) < min(person['role_ids'])
159 def can_view(self, person):
161 Returns true if we can view the specified person. We can
164 1. We are the person.
166 3. We are a PI and the person is at one of our sites.
169 assert isinstance(person, Person)
171 if self.can_update(person):
174 if 'pi' in self['roles']:
175 if set(self['site_ids']).intersection(person['site_ids']):
176 # Can view people with equal or higher role IDs
177 return min(self['role_ids']) <= min(person['role_ids'])
181 def add_role(self, role_id, commit = True):
183 Add role to existing account.
186 assert 'person_id' in self
188 person_id = self['person_id']
190 if role_id not in self['role_ids']:
191 self.api.db.do("INSERT INTO person_role (person_id, role_id)" \
192 " VALUES(%(person_id)d, %(role_id)d)",
198 self['role_ids'].append(role_id)
200 def remove_role(self, role_id, commit = True):
202 Remove role from existing account.
205 assert 'person_id' in self
207 person_id = self['person_id']
209 if role_id in self['role_ids']:
210 self.api.db.do("DELETE FROM person_role" \
211 " WHERE person_id = %(person_id)d" \
212 " AND role_id = %(role_id)d",
218 self['role_ids'].remove(role_id)
220 def add_key(self, key, commit = True):
222 Add key to existing account.
225 assert 'person_id' in self
226 assert isinstance(key, Key)
227 assert 'key_id' in key
229 person_id = self['person_id']
230 key_id = key['key_id']
232 if key_id not in self['key_ids']:
233 self.api.db.do("INSERT INTO person_key (person_id, key_id)" \
234 " VALUES(%(person_id)d, %(key_id)d)",
240 self['key_ids'].append(key_id)
242 def remove_key(self, key, commit = True):
244 Remove key from existing account.
247 assert 'person_id' in self
248 assert isinstance(key, Key)
249 assert 'key_id' in key
251 person_id = self['person_id']
252 key_id = key['key_id']
254 if key_id in self['key_ids']:
255 self.api.db.do("DELETE FROM person_key" \
256 " WHERE person_id = %(person_id)d" \
257 " AND key_id = %(key_id)d",
263 self['key_ids'].remove(key_id)
265 def set_primary_site(self, site, commit = True):
267 Set the primary site for an existing account.
270 assert 'person_id' in self
271 assert isinstance(site, PLC.Sites.Site)
272 assert 'site_id' in site
274 person_id = self['person_id']
275 site_id = site['site_id']
276 self.api.db.do("UPDATE person_site SET is_primary = False" \
277 " WHERE person_id = %(person_id)d",
279 self.api.db.do("UPDATE person_site SET is_primary = True" \
280 " WHERE person_id = %(person_id)d" \
281 " AND site_id = %(site_id)d",
287 assert 'site_ids' in self
288 assert site_id in self['site_ids']
290 # Make sure that the primary site is first in the list
291 self['site_ids'].remove(site_id)
292 self['site_ids'].insert(0, site_id)
294 def send_initiate_password_reset_email(self):
295 # email user next step instructions
297 to_addr[self['email']] = "%s %s" % \
298 (self['first_name'], self['last_name'])
300 from_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
301 "%s %s" % ('Planetlab', 'Support')
304 messages = Messages(self.api, ['ASSWORD_RESET_INITIATE'])
306 print >> log, "No such message template"
309 message = messages[0]
310 subject = message['subject']
311 template = message['template'] % \
312 (self.api.config.PLC_WWW_HOST,
313 self['verification_key'], self['person_id'],
314 self.api.config.PLC_MAIL_SUPPORT_ADDRESS,
315 self.api.config.PLC_WWW_HOST)
317 self.api.mailer.mail(to_addr, None, from_addr, subject, template)
319 def send_account_registered_email(self, site):
324 from_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
325 "%s %s" % ('Planetlab', 'Support')
328 user_full_name = "%s %s" % (self['first_name'], self['last_name'])
329 to_addr[self['email']] = "%s" % user_full_name
331 # if the account had a admin role or a pi role, email support.
332 if set(['admin', 'pi']).intersection(self['roles']):
333 to_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
334 "%s %s" % ('Planetlab', 'Support')
337 site_persons = Persons(self.api, site['person_ids'])
338 for person in site_persons:
339 if 'pi' in person['roles'] and not person['email'] in to_addr.keys():
340 cc_addr[person['email']] = "%s %s" % \
341 (person['first_name'], person['last_name'])
344 messages = Messages(self.api, ['ACCOUNT_REGISTERED'])
346 print >> log, "No such message template"
349 message = messages[0]
350 subject = message['subject'] % (user_full_name, site['name'])
351 template = message['template'] % \
352 (user_full_name, site['name'], ", ".join(self['roles']),
353 self.api.config.PLC_WWW_HOST, self['person_id'],
354 self.api.config.PLC_MAIL_SUPPORT_ADDRESS,
355 self.api.config.PLC_WWW_HOST)
357 self.api.mailer.mail(to_addr, cc_addr, from_addr, subject, template)
359 def delete(self, commit = True):
361 Delete existing account.
365 keys = Keys(self.api, self['key_ids'])
367 key.delete(commit = False)
369 # Clean up miscellaneous join tables
370 for table in self.join_tables:
371 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
372 (table, self['person_id']))
375 self['deleted'] = True
378 class Persons(Table):
380 Representation of row(s) from the persons table in the
384 def __init__(self, api, person_filter = None, columns = None):
385 Table.__init__(self, api, Person, columns)
387 sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
388 ", ".join(self.columns)
390 if person_filter is not None:
391 if isinstance(person_filter, (list, tuple, set)):
392 # Separate the list into integers and strings
393 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
394 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
395 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
396 sql += " AND (%s)" % person_filter.sql(api, "OR")
397 elif isinstance(person_filter, dict):
398 person_filter = Filter(Person.fields, person_filter)
399 sql += " AND (%s)" % person_filter.sql(api, "AND")