(*) direct cross refs redefined as NOT NULL in the database
[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.21 2006/11/25 09:35:36 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     def can_update(self, person):
120         """
121         Returns true if we can update the specified person. We can
122         update a person if:
123
124         1. We are the person.
125         2. We are an admin.
126         3. We are a PI and the person is a user or tech or at
127            one of our sites.
128         """
129
130         assert isinstance(person, Person)
131
132         if self['person_id'] == person['person_id']:
133             return True
134
135         if 'admin' in self['roles']:
136             return True
137
138         if 'pi' in self['roles']:
139             if set(self['site_ids']).intersection(person['site_ids']):
140                 # Can update people with higher role IDs
141                 return min(self['role_ids']) < min(person['role_ids'])
142
143         return False
144
145     def can_view(self, person):
146         """
147         Returns true if we can view the specified person. We can
148         view a person if:
149
150         1. We are the person.
151         2. We are an admin.
152         3. We are a PI and the person is at one of our sites.
153         """
154
155         assert isinstance(person, Person)
156
157         if self.can_update(person):
158             return True
159
160         if 'pi' in self['roles']:
161             if set(self['site_ids']).intersection(person['site_ids']):
162                 # Can view people with equal or higher role IDs
163                 return min(self['role_ids']) <= min(person['role_ids'])
164
165         return False
166
167     def add_role(self, role_id, commit = True):
168         """
169         Add role to existing account.
170         """
171
172         assert 'person_id' in self
173
174         person_id = self['person_id']
175
176         if role_id not in self['role_ids']:
177             self.api.db.do("INSERT INTO person_role (person_id, role_id)" \
178                            " VALUES(%(person_id)d, %(role_id)d)",
179                            locals())
180
181             if commit:
182                 self.api.db.commit()
183
184             self['role_ids'].append(role_id)
185
186     def remove_role(self, role_id, commit = True):
187         """
188         Remove role from existing account.
189         """
190
191         assert 'person_id' in self
192
193         person_id = self['person_id']
194
195         if role_id in self['role_ids']:
196             self.api.db.do("DELETE FROM person_role" \
197                            " WHERE person_id = %(person_id)d" \
198                            " AND role_id = %(role_id)d",
199                            locals())
200
201             if commit:
202                 self.api.db.commit()
203
204             self['role_ids'].remove(role_id)
205  
206     def add_key(self, key, commit = True):
207         """
208         Add key to existing account.
209         """
210
211         assert 'person_id' in self
212         assert isinstance(key, Key)
213         assert 'key_id' in key
214
215         person_id = self['person_id']
216         key_id = key['key_id']
217
218         if key_id not in self['key_ids']:
219             self.api.db.do("INSERT INTO person_key (person_id, key_id)" \
220                            " VALUES(%(person_id)d, %(key_id)d)",
221                            locals())
222
223             if commit:
224                 self.api.db.commit()
225
226             self['key_ids'].append(key_id)
227
228     def remove_key(self, key, commit = True):
229         """
230         Remove key from existing account.
231         """
232
233         assert 'person_id' in self
234         assert isinstance(key, Key)
235         assert 'key_id' in key
236
237         person_id = self['person_id']
238         key_id = key['key_id']
239
240         if key_id in self['key_ids']:
241             self.api.db.do("DELETE FROM person_key" \
242                            " WHERE person_id = %(person_id)d" \
243                            " AND key_id = %(key_id)d",
244                            locals())
245
246             if commit:
247                 self.api.db.commit()
248
249             self['key_ids'].remove(key_id)
250
251     def set_primary_site(self, site, commit = True):
252         """
253         Set the primary site for an existing account.
254         """
255
256         assert 'person_id' in self
257         assert isinstance(site, PLC.Sites.Site)
258         assert 'site_id' in site
259
260         person_id = self['person_id']
261         site_id = site['site_id']
262         self.api.db.do("UPDATE person_site SET is_primary = False" \
263                        " WHERE person_id = %(person_id)d",
264                        locals())
265         self.api.db.do("UPDATE person_site SET is_primary = True" \
266                        " WHERE person_id = %(person_id)d" \
267                        " AND site_id = %(site_id)d",
268                        locals())
269
270         if commit:
271             self.api.db.commit()
272
273         assert 'site_ids' in self
274         assert site_id in self['site_ids']
275
276         # Make sure that the primary site is first in the list
277         self['site_ids'].remove(site_id)
278         self['site_ids'].insert(0, site_id)
279
280     def delete(self, commit = True):
281         """
282         Delete existing account.
283         """
284
285         # Delete all keys
286         keys = Keys(self.api, self['key_ids'])
287         for key in keys:
288             key.delete(commit = False)
289
290         # Clean up miscellaneous join tables
291         for table in self.join_tables:
292             self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
293                            (table, self['person_id']))
294
295         # Mark as deleted
296         self['deleted'] = True
297         self.sync(commit)
298
299 class Persons(Table):
300     """
301     Representation of row(s) from the persons table in the
302     database.
303     """
304
305     def __init__(self, api, person_filter = None, columns = None):
306         Table.__init__(self, api, Person, columns)
307
308         sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
309               ", ".join(self.columns)
310
311         if person_filter is not None:
312             if isinstance(person_filter, (list, tuple, set)):
313                 # Separate the list into integers and strings
314                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
315                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
316                 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
317                 sql += " AND (%s)" % person_filter.sql(api, "OR")
318             elif isinstance(person_filter, dict):
319                 person_filter = Filter(Person.fields, person_filter)
320                 sql += " AND (%s)" % person_filter.sql(api, "AND")
321
322         self.selectall(sql)