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