- if the appropriate message template doesn't exist log error and exit without throwi...
[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 # $Id: Persons.py,v 1.28 2007/01/05 19:50:20 tmack Exp $
8 #
9
10 from types import StringTypes
11 from datetime import datetime
12 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.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
25 import PLC.Sites
26
27 class Person(Row):
28     """
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().
32     """
33
34     table_name = 'persons'
35     primary_key = 'person_id'
36     join_tables = ['person_role', 'person_site', 'slice_person', 'person_session']
37     fields = {
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),
58         }
59
60     # for Cache
61     class_key = 'email'
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'
67     foreign_xrefs = [
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"),
72 ]
73
74     def validate_email(self, email):
75         """
76         Validate email address. Stolen from Mailman.
77         """
78
79         invalid_email = PLCInvalidArgument("Invalid e-mail address")
80         email_badchars = r'[][()<>|;^,\200-\377]'
81
82         # Pretty minimal, cheesy check.  We could do better...
83         if not email or email.count(' ') > 0:
84             raise invalid_email
85         if re.search(email_badchars, email) or email[0] == '-':
86             raise invalid_email
87
88         email = email.lower()
89         at_sign = email.find('@')
90         if at_sign < 1:
91             raise invalid_email
92         user = email[:at_sign]
93         rest = email[at_sign+1:]
94         domain = rest.split('.')
95
96         # This means local, unqualified addresses, are no allowed
97         if not domain:
98             raise invalid_email
99         if len(domain) < 2:
100             raise invalid_email
101
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"
106
107         return email
108
109     def validate_password(self, password):
110         """
111         Encrypt password if necessary before committing to the
112         database.
113         """
114
115         magic = "$1$"
116
117         if len(password) > len(magic) and \
118            password[0:len(magic)] == magic:
119             return password
120         else:
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 + "$")
125
126     # timestamps
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)
132
133     def can_update(self, person):
134         """
135         Returns true if we can update the specified person. We can
136         update a person if:
137
138         1. We are the person.
139         2. We are an admin.
140         3. We are a PI and the person is a user or tech or at
141            one of our sites.
142         """
143
144         assert isinstance(person, Person)
145
146         if self['person_id'] == person['person_id']:
147             return True
148
149         if 'admin' in self['roles']:
150             return True
151
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'])
156
157         return False
158
159     def can_view(self, person):
160         """
161         Returns true if we can view the specified person. We can
162         view a person if:
163
164         1. We are the person.
165         2. We are an admin.
166         3. We are a PI and the person is at one of our sites.
167         """
168
169         assert isinstance(person, Person)
170
171         if self.can_update(person):
172             return True
173
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'])
178
179         return False
180
181     def add_role(self, role_id, commit = True):
182         """
183         Add role to existing account.
184         """
185
186         assert 'person_id' in self
187
188         person_id = self['person_id']
189
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)",
193                            locals())
194
195             if commit:
196                 self.api.db.commit()
197
198             self['role_ids'].append(role_id)
199
200     def remove_role(self, role_id, commit = True):
201         """
202         Remove role from existing account.
203         """
204
205         assert 'person_id' in self
206
207         person_id = self['person_id']
208
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",
213                            locals())
214
215             if commit:
216                 self.api.db.commit()
217
218             self['role_ids'].remove(role_id)
219  
220     def add_key(self, key, commit = True):
221         """
222         Add key to existing account.
223         """
224
225         assert 'person_id' in self
226         assert isinstance(key, Key)
227         assert 'key_id' in key
228
229         person_id = self['person_id']
230         key_id = key['key_id']
231
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)",
235                            locals())
236
237             if commit:
238                 self.api.db.commit()
239
240             self['key_ids'].append(key_id)
241
242     def remove_key(self, key, commit = True):
243         """
244         Remove key from existing account.
245         """
246
247         assert 'person_id' in self
248         assert isinstance(key, Key)
249         assert 'key_id' in key
250
251         person_id = self['person_id']
252         key_id = key['key_id']
253
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",
258                            locals())
259
260             if commit:
261                 self.api.db.commit()
262
263             self['key_ids'].remove(key_id)
264
265     def set_primary_site(self, site, commit = True):
266         """
267         Set the primary site for an existing account.
268         """
269
270         assert 'person_id' in self
271         assert isinstance(site, PLC.Sites.Site)
272         assert 'site_id' in site
273
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",
278                        locals())
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",
282                        locals())
283
284         if commit:
285             self.api.db.commit()
286
287         assert 'site_ids' in self
288         assert site_id in self['site_ids']
289
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)
293
294     def send_initiate_password_reset_email(self):
295         # email user next step instructions
296         to_addr = {}
297         to_addr[self['email']] = "%s %s" % \
298             (self['first_name'], self['last_name'])
299         from_addr = {}
300         from_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
301         "%s %s" % ('Planetlab', 'Support')
302
303         # fill in template
304         messages = Messages(self.api, ['ASSWORD_RESET_INITIATE'])
305         if not messages:
306             print >> log, "No such message template"
307             return 1
308
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)
316
317         self.api.mailer.mail(to_addr, None, from_addr, subject, template)
318     
319     def send_account_registered_email(self, site):
320         
321         to_addr = {}
322         cc_addr = {}
323         from_addr = {}
324         from_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
325         "%s %s" % ('Planetlab', 'Support')
326
327         # email user
328         user_full_name = "%s %s" % (self['first_name'], self['last_name'])
329         to_addr[self['email']] = "%s" % user_full_name
330
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')
335         
336         # cc site pi's
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'])
342
343         # fill in template
344         messages = Messages(self.api, ['ACCOUNT_REGISTERED'])
345         if not messages:
346             print >> log, "No such message template"
347             return 1
348
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)
356                                 
357         self.api.mailer.mail(to_addr, cc_addr, from_addr, subject, template)
358
359     def delete(self, commit = True):
360         """
361         Delete existing account.
362         """
363
364         # Delete all keys
365         keys = Keys(self.api, self['key_ids'])
366         for key in keys:
367             key.delete(commit = False)
368
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']))
373
374         # Mark as deleted
375         self['deleted'] = True
376         self.sync(commit)
377
378 class Persons(Table):
379     """
380     Representation of row(s) from the persons table in the
381     database.
382     """
383
384     def __init__(self, api, person_filter = None, columns = None):
385         Table.__init__(self, api, Person, columns)
386
387         sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
388               ", ".join(self.columns)
389
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")
400         self.selectall(sql)