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