- added Person.send_initiate_password_reset_email method
[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.26 2007/01/05 15:56:16 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.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
24 import PLC.Sites
25
26 class Person(Row):
27     """
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().
31     """
32
33     table_name = 'persons'
34     primary_key = 'person_id'
35     join_tables = ['person_role', 'person_site', 'slice_person', 'person_session']
36     fields = {
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),
57         }
58
59     # for Cache
60     class_key = 'email'
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'
66     foreign_xrefs = [
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"),
71 ]
72
73     def validate_email(self, email):
74         """
75         Validate email address. Stolen from Mailman.
76         """
77
78         invalid_email = PLCInvalidArgument("Invalid e-mail address")
79         email_badchars = r'[][()<>|;^,\200-\377]'
80
81         # Pretty minimal, cheesy check.  We could do better...
82         if not email or email.count(' ') > 0:
83             raise invalid_email
84         if re.search(email_badchars, email) or email[0] == '-':
85             raise invalid_email
86
87         email = email.lower()
88         at_sign = email.find('@')
89         if at_sign < 1:
90             raise invalid_email
91         user = email[:at_sign]
92         rest = email[at_sign+1:]
93         domain = rest.split('.')
94
95         # This means local, unqualified addresses, are no allowed
96         if not domain:
97             raise invalid_email
98         if len(domain) < 2:
99             raise invalid_email
100
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"
105
106         return email
107
108     def validate_password(self, password):
109         """
110         Encrypt password if necessary before committing to the
111         database.
112         """
113
114         magic = "$1$"
115
116         if len(password) > len(magic) and \
117            password[0:len(magic)] == magic:
118             return password
119         else:
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 + "$")
124
125     # timestamps
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)
131
132     def can_update(self, person):
133         """
134         Returns true if we can update the specified person. We can
135         update a person if:
136
137         1. We are the person.
138         2. We are an admin.
139         3. We are a PI and the person is a user or tech or at
140            one of our sites.
141         """
142
143         assert isinstance(person, Person)
144
145         if self['person_id'] == person['person_id']:
146             return True
147
148         if 'admin' in self['roles']:
149             return True
150
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'])
155
156         return False
157
158     def can_view(self, person):
159         """
160         Returns true if we can view the specified person. We can
161         view a person if:
162
163         1. We are the person.
164         2. We are an admin.
165         3. We are a PI and the person is at one of our sites.
166         """
167
168         assert isinstance(person, Person)
169
170         if self.can_update(person):
171             return True
172
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'])
177
178         return False
179
180     def add_role(self, role_id, commit = True):
181         """
182         Add role to existing account.
183         """
184
185         assert 'person_id' in self
186
187         person_id = self['person_id']
188
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)",
192                            locals())
193
194             if commit:
195                 self.api.db.commit()
196
197             self['role_ids'].append(role_id)
198
199     def remove_role(self, role_id, commit = True):
200         """
201         Remove role from existing account.
202         """
203
204         assert 'person_id' in self
205
206         person_id = self['person_id']
207
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",
212                            locals())
213
214             if commit:
215                 self.api.db.commit()
216
217             self['role_ids'].remove(role_id)
218  
219     def add_key(self, key, commit = True):
220         """
221         Add key to existing account.
222         """
223
224         assert 'person_id' in self
225         assert isinstance(key, Key)
226         assert 'key_id' in key
227
228         person_id = self['person_id']
229         key_id = key['key_id']
230
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)",
234                            locals())
235
236             if commit:
237                 self.api.db.commit()
238
239             self['key_ids'].append(key_id)
240
241     def remove_key(self, key, commit = True):
242         """
243         Remove key from existing account.
244         """
245
246         assert 'person_id' in self
247         assert isinstance(key, Key)
248         assert 'key_id' in key
249
250         person_id = self['person_id']
251         key_id = key['key_id']
252
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",
257                            locals())
258
259             if commit:
260                 self.api.db.commit()
261
262             self['key_ids'].remove(key_id)
263
264     def set_primary_site(self, site, commit = True):
265         """
266         Set the primary site for an existing account.
267         """
268
269         assert 'person_id' in self
270         assert isinstance(site, PLC.Sites.Site)
271         assert 'site_id' in site
272
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",
277                        locals())
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",
281                        locals())
282
283         if commit:
284             self.api.db.commit()
285
286         assert 'site_ids' in self
287         assert site_id in self['site_ids']
288
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)
292
293     def send_initiate_password_reset_email(self):
294         # email user next step instructions
295         to_addr = {}
296         to_addr[self['email']] = "%s %s" % \
297             (self['first_name'], self['last_name'])
298         from_addr = {}
299         from_addr[self.api.config.PLC_MAIL_SUPPORT_ADDRESS] = \
300         "%s %s" % ('Planetlab', 'Support')
301         messages = Messages(self.api, ['PASSWORD_RESET_INITIATE'])
302         if not messages:
303                 raise PLCAPIError, "Email template not found"
304         message = messages[0]
305         subject = message['subject']
306         template = message['template'] % \
307                 (self.api.config.PLC_WWW_HOST,
308                  self['verification_key'], self['person_id'],
309                  self.api.config.PLC_MAIL_SUPPORT_ADDRESS,
310                  self.api.config.PLC_WWW_HOST)
311
312         self.api.mailer.mail(to_addr, None, from_addr, subject, template)
313
314
315     def delete(self, commit = True):
316         """
317         Delete existing account.
318         """
319
320         # Delete all keys
321         keys = Keys(self.api, self['key_ids'])
322         for key in keys:
323             key.delete(commit = False)
324
325         # Clean up miscellaneous join tables
326         for table in self.join_tables:
327             self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
328                            (table, self['person_id']))
329
330         # Mark as deleted
331         self['deleted'] = True
332         self.sync(commit)
333
334 class Persons(Table):
335     """
336     Representation of row(s) from the persons table in the
337     database.
338     """
339
340     def __init__(self, api, person_filter = None, columns = None):
341         Table.__init__(self, api, Person, columns)
342
343         sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
344               ", ".join(self.columns)
345
346         if person_filter is not None:
347             if isinstance(person_filter, (list, tuple, set)):
348                 # Separate the list into integers and strings
349                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
350                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
351                 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
352                 sql += " AND (%s)" % person_filter.sql(api, "OR")
353             elif isinstance(person_filter, dict):
354                 person_filter = Filter(Person.fields, person_filter)
355                 sql += " AND (%s)" % person_filter.sql(api, "AND")
356         self.selectall(sql)