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