- added 'verification_key', 'verification_expires' to Person.fields
[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.25 2007/01/05 15:54:39 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 import PLC.Sites
24
25 class Person(Row):
26     """
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().
30     """
31
32     table_name = 'persons'
33     primary_key = 'person_id'
34     join_tables = ['person_role', 'person_site', 'slice_person', 'person_session']
35     fields = {
36         'person_id': Parameter(int, "Account 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),
47         'verification_expires': Parameter(str, "Date/Time when verification_key expires", max = 254),
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 at which this slice was created", nullok = True),
56         }
57
58     # for Cache
59     class_key = 'email'
60     foreign_fields = ['first_name', 'last_name', 'title', 'email', 'phone', 'url',
61                       'bio', 'enabled', 'password', ]
62     # forget about these ones, they are read-only anyway
63     # handling them causes Cache to re-sync all over again 
64     # 'last_updated', 'date_created'
65     foreign_xrefs = [
66         {'field' : 'key_ids',  'class': 'Key',  'table' : 'person_key' } ,
67         {'field' : 'site_ids', 'class': 'Site', 'table' : 'person_site'},
68 #       xxx this is not handled by Cache yet
69 #        'role_ids': Parameter([int], "List of role identifiers"),
70 ]
71
72     def validate_email(self, email):
73         """
74         Validate email address. Stolen from Mailman.
75         """
76
77         invalid_email = PLCInvalidArgument("Invalid e-mail address")
78         email_badchars = r'[][()<>|;^,\200-\377]'
79
80         # Pretty minimal, cheesy check.  We could do better...
81         if not email or email.count(' ') > 0:
82             raise invalid_email
83         if re.search(email_badchars, email) or email[0] == '-':
84             raise invalid_email
85
86         email = email.lower()
87         at_sign = email.find('@')
88         if at_sign < 1:
89             raise invalid_email
90         user = email[:at_sign]
91         rest = email[at_sign+1:]
92         domain = rest.split('.')
93
94         # This means local, unqualified addresses, are no allowed
95         if not domain:
96             raise invalid_email
97         if len(domain) < 2:
98             raise invalid_email
99
100         conflicts = Persons(self.api, [email])
101         for person in conflicts:
102             if 'person_id' not in self or self['person_id'] != person['person_id']:
103                 raise PLCInvalidArgument, "E-mail address already in use"
104
105         return email
106
107     def validate_password(self, password):
108         """
109         Encrypt password if necessary before committing to the
110         database.
111         """
112
113         magic = "$1$"
114
115         if len(password) > len(magic) and \
116            password[0:len(magic)] == magic:
117             return password
118         else:
119             # Generate a somewhat unique 8 character salt string
120             salt = str(time.time()) + str(Random().random())
121             salt = md5.md5(salt).hexdigest()[:8] 
122             return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
123
124     # timestamps
125     # verification_expires in the DB but not exposed here
126     def validate_date_created (self, timestamp):
127         return self.validate_timestamp (timestamp)
128     def validate_last_updated (self, timestamp):
129         return self.validate_timestamp (timestamp)
130
131     def can_update(self, person):
132         """
133         Returns true if we can update the specified person. We can
134         update a person if:
135
136         1. We are the person.
137         2. We are an admin.
138         3. We are a PI and the person is a user or tech or at
139            one of our sites.
140         """
141
142         assert isinstance(person, Person)
143
144         if self['person_id'] == person['person_id']:
145             return True
146
147         if 'admin' in self['roles']:
148             return True
149
150         if 'pi' in self['roles']:
151             if set(self['site_ids']).intersection(person['site_ids']):
152                 # Can update people with higher role IDs
153                 return min(self['role_ids']) < min(person['role_ids'])
154
155         return False
156
157     def can_view(self, person):
158         """
159         Returns true if we can view the specified person. We can
160         view a person if:
161
162         1. We are the person.
163         2. We are an admin.
164         3. We are a PI and the person is at one of our sites.
165         """
166
167         assert isinstance(person, Person)
168
169         if self.can_update(person):
170             return True
171
172         if 'pi' in self['roles']:
173             if set(self['site_ids']).intersection(person['site_ids']):
174                 # Can view people with equal or higher role IDs
175                 return min(self['role_ids']) <= min(person['role_ids'])
176
177         return False
178
179     def add_role(self, role_id, commit = True):
180         """
181         Add role to existing account.
182         """
183
184         assert 'person_id' in self
185
186         person_id = self['person_id']
187
188         if role_id not in self['role_ids']:
189             self.api.db.do("INSERT INTO person_role (person_id, role_id)" \
190                            " VALUES(%(person_id)d, %(role_id)d)",
191                            locals())
192
193             if commit:
194                 self.api.db.commit()
195
196             self['role_ids'].append(role_id)
197
198     def remove_role(self, role_id, commit = True):
199         """
200         Remove role from existing account.
201         """
202
203         assert 'person_id' in self
204
205         person_id = self['person_id']
206
207         if role_id in self['role_ids']:
208             self.api.db.do("DELETE FROM person_role" \
209                            " WHERE person_id = %(person_id)d" \
210                            " AND role_id = %(role_id)d",
211                            locals())
212
213             if commit:
214                 self.api.db.commit()
215
216             self['role_ids'].remove(role_id)
217  
218     def add_key(self, key, commit = True):
219         """
220         Add key to existing account.
221         """
222
223         assert 'person_id' in self
224         assert isinstance(key, Key)
225         assert 'key_id' in key
226
227         person_id = self['person_id']
228         key_id = key['key_id']
229
230         if key_id not in self['key_ids']:
231             self.api.db.do("INSERT INTO person_key (person_id, key_id)" \
232                            " VALUES(%(person_id)d, %(key_id)d)",
233                            locals())
234
235             if commit:
236                 self.api.db.commit()
237
238             self['key_ids'].append(key_id)
239
240     def remove_key(self, key, commit = True):
241         """
242         Remove key from existing account.
243         """
244
245         assert 'person_id' in self
246         assert isinstance(key, Key)
247         assert 'key_id' in key
248
249         person_id = self['person_id']
250         key_id = key['key_id']
251
252         if key_id in self['key_ids']:
253             self.api.db.do("DELETE FROM person_key" \
254                            " WHERE person_id = %(person_id)d" \
255                            " AND key_id = %(key_id)d",
256                            locals())
257
258             if commit:
259                 self.api.db.commit()
260
261             self['key_ids'].remove(key_id)
262
263     def set_primary_site(self, site, commit = True):
264         """
265         Set the primary site for an existing account.
266         """
267
268         assert 'person_id' in self
269         assert isinstance(site, PLC.Sites.Site)
270         assert 'site_id' in site
271
272         person_id = self['person_id']
273         site_id = site['site_id']
274         self.api.db.do("UPDATE person_site SET is_primary = False" \
275                        " WHERE person_id = %(person_id)d",
276                        locals())
277         self.api.db.do("UPDATE person_site SET is_primary = True" \
278                        " WHERE person_id = %(person_id)d" \
279                        " AND site_id = %(site_id)d",
280                        locals())
281
282         if commit:
283             self.api.db.commit()
284
285         assert 'site_ids' in self
286         assert site_id in self['site_ids']
287
288         # Make sure that the primary site is first in the list
289         self['site_ids'].remove(site_id)
290         self['site_ids'].insert(0, site_id)
291
292     def delete(self, commit = True):
293         """
294         Delete existing account.
295         """
296
297         # Delete all keys
298         keys = Keys(self.api, self['key_ids'])
299         for key in keys:
300             key.delete(commit = False)
301
302         # Clean up miscellaneous join tables
303         for table in self.join_tables:
304             self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
305                            (table, self['person_id']))
306
307         # Mark as deleted
308         self['deleted'] = True
309         self.sync(commit)
310
311 class Persons(Table):
312     """
313     Representation of row(s) from the persons table in the
314     database.
315     """
316
317     def __init__(self, api, person_filter = None, columns = None):
318         Table.__init__(self, api, Person, columns)
319
320         sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
321               ", ".join(self.columns)
322
323         if person_filter is not None:
324             if isinstance(person_filter, (list, tuple, set)):
325                 # Separate the list into integers and strings
326                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
327                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
328                 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
329                 sql += " AND (%s)" % person_filter.sql(api, "OR")
330             elif isinstance(person_filter, dict):
331                 person_filter = Filter(Person.fields, person_filter)
332                 sql += " AND (%s)" % person_filter.sql(api, "AND")
333         self.selectall(sql)