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