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