8276c06f644339213d5c0e062b911435d5596313
[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
8 from types import StringTypes
9 try:
10     from hashlib import md5
11 except ImportError:
12     from md5 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.Debug import log
20 from PLC.Parameter import Parameter, Mixed
21 from PLC.Filter import Filter
22 from PLC.Table import Row, Table
23 from PLC.Roles import Role, Roles
24 from PLC.Keys import Key, Keys
25 from PLC.Messages import Message, Messages
26
27 class Person(Row):
28     """
29     Representation of a row in the persons table. To use, optionally
30     instantiate with a dict of values. Update as you would a
31     dict. Commit to the database with sync().
32     """
33
34     table_name = 'persons'
35     primary_key = 'person_id'
36     join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
37     fields = {
38         'person_id': Parameter(int, "User identifier"),
39         'first_name': Parameter(str, "Given name", max = 128),
40         'last_name': Parameter(str, "Surname", max = 128),
41         'title': Parameter(str, "Title", max = 128, nullok = True),
42         'email': Parameter(str, "Primary e-mail address", max = 254),
43         'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
44         'url': Parameter(str, "Home page", max = 254, nullok = True),
45         'bio': Parameter(str, "Biography", max = 254, nullok = True),
46         'enabled': Parameter(bool, "Has been enabled"),
47         'password': Parameter(str, "Account password in crypt() form", max = 254),
48         'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
49         'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
50         'last_updated': Parameter(int, "Date and time of last update", ro = True),
51         'date_created': Parameter(int, "Date and time when account was created", ro = True),
52         'role_ids': Parameter([int], "List of role identifiers"),
53         'roles': Parameter([str], "List of roles"),
54         'site_ids': Parameter([int], "List of site identifiers"),
55         'key_ids': Parameter([int], "List of key identifiers"),
56         'slice_ids': Parameter([int], "List of slice identifiers"),
57         'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
58         'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
59         'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
60         }
61     related_fields = {
62         'roles': [Mixed(Parameter(int, "Role identifier"),
63                         Parameter(str, "Role name"))],
64         'sites': [Mixed(Parameter(int, "Site identifier"),
65                         Parameter(str, "Site name"))],
66         'keys': [Mixed(Parameter(int, "Key identifier"),
67                        Filter(Key.fields))],
68         'slices': [Mixed(Parameter(int, "Slice identifier"),
69                          Parameter(str, "Slice name"))]
70         }
71     view_tags_name = "view_person_tags"
72     # tags are used by the Add/Get/Update methods to expose tags
73     # this is initialized here and updated by the accessors factory
74     tags = { }
75
76     def validate_email(self, email):
77         """
78         Validate email address. Stolen from Mailman.
79         """
80         email = email.lower()
81         invalid_email = PLCInvalidArgument("Invalid e-mail address %s"%email)
82
83         if not email:
84             raise invalid_email
85
86         email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
87         if not email_re.match(email):
88             raise invalid_email
89
90         # check only against users on the same peer
91         if 'peer_id' in self:
92             namespace_peer_id = self['peer_id']
93         else:
94             namespace_peer_id = None
95
96         conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
97
98         for person in conflicts:
99             if 'person_id' not in self or self['person_id'] != person['person_id']:
100                 raise PLCInvalidArgument, "E-mail address already in use"
101
102         return email
103
104     def validate_password(self, password):
105         """
106         Encrypt password if necessary before committing to the
107         database.
108         """
109
110         magic = "$1$"
111
112         if len(password) > len(magic) and \
113            password[0:len(magic)] == magic:
114             return password
115         else:
116             # Generate a somewhat unique 8 character salt string
117             salt = str(time.time()) + str(Random().random())
118             salt = md5(salt).hexdigest()[:8]
119             return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
120
121     validate_date_created = Row.validate_timestamp
122     validate_last_updated = Row.validate_timestamp
123     validate_verification_expires = Row.validate_timestamp
124
125     def can_update(self, person):
126         """
127         Returns true if we can update the specified person. We can
128         update a person if:
129
130         1. We are the person.
131         2. We are an admin.
132         3. We are a PI and the person is a user or tech or at
133            one of our sites.
134         """
135
136         assert isinstance(person, Person)
137
138         if self['person_id'] == person['person_id']:
139             return True
140
141         if 'admin' in self['roles']:
142             return True
143
144         if 'pi' in self['roles']:
145             if set(self['site_ids']).intersection(person['site_ids']):
146                 # non-admin users cannot update a person who is neither a PI or ADMIN
147                 return (not set(['pi','admin']).intersection(person['roles']))
148
149         return False
150
151     def can_view(self, person):
152         """
153         Returns true if we can view the specified person. We can
154         view a person if:
155
156         1. We are the person.
157         2. We are an admin.
158         3. We are a PI or Tech and the person is at one of our sites.
159         """
160
161         assert isinstance(person, Person)
162
163         if self.can_update(person):
164             return True
165
166         # pis and techs can see all people on their site
167         if set(['pi','tech']).intersection(self['roles']):
168             if set(self['site_ids']).intersection(person['site_ids']):
169                 return True
170
171         return False
172
173     add_role = Row.add_object(Role, 'person_role')
174     remove_role = Row.remove_object(Role, 'person_role')
175
176     add_key = Row.add_object(Key, 'person_key')
177     remove_key = Row.remove_object(Key, 'person_key')
178
179     def set_primary_site(self, site, commit = True):
180         """
181         Set the primary site for an existing user.
182         """
183
184         assert 'person_id' in self
185         assert 'site_id' in site
186
187         person_id = self['person_id']
188         site_id = site['site_id']
189         self.api.db.do("UPDATE person_site SET is_primary = False" \
190                        " WHERE person_id = %(person_id)d",
191                        locals())
192         self.api.db.do("UPDATE person_site SET is_primary = True" \
193                        " WHERE person_id = %(person_id)d" \
194                        " AND site_id = %(site_id)d",
195                        locals())
196
197         if commit:
198             self.api.db.commit()
199
200         assert 'site_ids' in self
201         assert site_id in self['site_ids']
202
203         # Make sure that the primary site is first in the list
204         self['site_ids'].remove(site_id)
205         self['site_ids'].insert(0, site_id)
206
207     def update_last_updated(self, commit = True):
208         """
209         Update last_updated field with current time
210         """
211
212         assert 'person_id' in self
213         assert self.table_name
214
215         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
216                        " where person_id = %d" % (self['person_id']) )
217         self.sync(commit)
218
219     def associate_roles(self, auth, field, value):
220         """
221         Adds roles found in value list to this person (using AddRoleToPerson).
222         Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
223         """
224
225         assert 'role_ids' in self
226         assert 'person_id' in self
227         assert isinstance(value, list)
228
229         (role_ids, role_names) = self.separate_types(value)[0:2]
230
231         # Translate roles into role_ids
232         if role_names:
233             roles = Roles(self.api, role_names).dict('role_id')
234             role_ids += roles.keys()
235
236         # Add new ids, remove stale ids
237         if self['role_ids'] != role_ids:
238             from PLC.Methods.AddRoleToPerson import AddRoleToPerson
239             from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
240             new_roles = set(role_ids).difference(self['role_ids'])
241             stale_roles = set(self['role_ids']).difference(role_ids)
242
243             for new_role in new_roles:
244                 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
245             for stale_role in stale_roles:
246                 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
247
248
249     def associate_sites(self, auth, field, value):
250         """
251         Adds person to sites found in value list (using AddPersonToSite).
252         Deletes person from site not found in value list (using DeletePersonFromSite).
253         """
254
255         from PLC.Sites import Sites
256
257         assert 'site_ids' in self
258         assert 'person_id' in self
259         assert isinstance(value, list)
260
261         (site_ids, site_names) = self.separate_types(value)[0:2]
262
263         # Translate roles into role_ids
264         if site_names:
265             sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
266             site_ids += sites.keys()
267
268         # Add new ids, remove stale ids
269         if self['site_ids'] != site_ids:
270             from PLC.Methods.AddPersonToSite import AddPersonToSite
271             from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
272             new_sites = set(site_ids).difference(self['site_ids'])
273             stale_sites = set(self['site_ids']).difference(site_ids)
274
275             for new_site in new_sites:
276                 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
277             for stale_site in stale_sites:
278                 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
279
280
281     def associate_keys(self, auth, field, value):
282         """
283         Deletes key_ids not found in value list (using DeleteKey).
284         Adds key if key_fields w/o key_id is found (using AddPersonKey).
285         Updates key if key_fields w/ key_id is found (using UpdateKey).
286         """
287         assert 'key_ids' in self
288         assert 'person_id' in self
289         assert isinstance(value, list)
290
291         (key_ids, blank, keys) = self.separate_types(value)
292
293         if self['key_ids'] != key_ids:
294             from PLC.Methods.DeleteKey import DeleteKey
295             stale_keys = set(self['key_ids']).difference(key_ids)
296
297             for stale_key in stale_keys:
298                 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
299
300         if keys:
301             from PLC.Methods.AddPersonKey import AddPersonKey
302             from PLC.Methods.UpdateKey import UpdateKey
303             updated_keys = filter(lambda key: 'key_id' in key, keys)
304             added_keys = filter(lambda key: 'key_id' not in key, keys)
305
306             for key in added_keys:
307                 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
308             for key in updated_keys:
309                 key_id = key.pop('key_id')
310                 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
311
312
313     def associate_slices(self, auth, field, value):
314         """
315         Adds person to slices found in value list (using AddPersonToSlice).
316         Deletes person from slices found in value list (using DeletePersonFromSlice).
317         """
318
319         from PLC.Slices import Slices
320
321         assert 'slice_ids' in self
322         assert 'person_id' in self
323         assert isinstance(value, list)
324
325         (slice_ids, slice_names) = self.separate_types(value)[0:2]
326
327         # Translate roles into role_ids
328         if slice_names:
329             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
330             slice_ids += slices.keys()
331
332         # Add new ids, remove stale ids
333         if self['slice_ids'] != slice_ids:
334             from PLC.Methods.AddPersonToSlice import AddPersonToSlice
335             from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
336             new_slices = set(slice_ids).difference(self['slice_ids'])
337             stale_slices = set(self['slice_ids']).difference(slice_ids)
338
339             for new_slice in new_slices:
340                 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
341             for stale_slice in stale_slices:
342                 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
343
344
345     def delete(self, commit = True):
346         """
347         Delete existing user.
348         """
349
350         # Delete all keys
351         keys = Keys(self.api, self['key_ids'])
352         for key in keys:
353             key.delete(commit = False)
354
355         # Clean up miscellaneous join tables
356         for table in self.join_tables:
357             self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
358                            (table, self['person_id']))
359
360         # Mark as deleted
361         self['deleted'] = True
362
363         # delete will fail if timestamp fields aren't validated, so lets remove them
364         for field in ['verification_expires', 'date_created', 'last_updated']:
365             if field in self:
366                 self.pop(field)
367
368         # don't validate, so duplicates can be consistently removed
369         self.sync(commit, validate=False)
370
371 class Persons(Table):
372     """
373     Representation of row(s) from the persons table in the
374     database.
375     """
376
377     def __init__(self, api, person_filter = None, columns = None):
378         Table.__init__(self, api, Person, columns)
379
380         view = "view_persons"
381         for tagname in self.tag_columns:
382             view= "%s left join %s using (%s)"%(view,Person.tagvalue_view_name(tagname),
383                                                 Person.primary_key)
384
385         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
386             (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
387
388         if person_filter is not None:
389             if isinstance(person_filter, (list, tuple, set)):
390                 # Separate the list into integers and strings
391                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
392                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
393                 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
394                 sql += " AND (%s) %s" % person_filter.sql(api, "OR")
395             elif isinstance(person_filter, dict):
396                 allowed_fields=dict(Person.fields.items()+Person.tags.items())
397                 person_filter = Filter(allowed_fields, person_filter)
398                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
399             elif isinstance (person_filter, StringTypes):
400                 person_filter = Filter(Person.fields, {'email':person_filter})
401                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
402             elif isinstance (person_filter, (int, long)):
403                 person_filter = Filter(Person.fields, {'person_id':person_filter})
404                 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
405             else:
406                 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
407
408         self.selectall(sql)