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