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