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