1 # Copyright (c) 2003 Richard Jones (richard@mechanicalcat.net)
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining a copy
4 # of this software and associated documentation files (the "Software"), to deal
5 # in the Software without restriction, including without limitation the rights
6 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 # copies of the Software, and to permit persons to whom the Software is
8 # furnished to do so, subject to the following conditions:
9 #
10 # The above copyright notice and this permission notice shall be included in
11 # all copies or substantial portions of the Software.
12 #
13 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 # SOFTWARE.
20 #
21 #$Id: userauditor.py,v 1.9 2007-09-12 21:11:13 jpend Exp $
23 import re
25 # regular expression thanks to: http://www.regular-expressions.info/email.html
26 # this is the "99.99% solution for syntax only".
27 email_regexp = (r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*", r"(localhost|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9]))")
28 email_rfc = re.compile('^' + email_regexp[0] + '@' + email_regexp[1] + '$', re.IGNORECASE)
29 email_local = re.compile('^' + email_regexp[0] + '$', re.IGNORECASE)
31 def valid_address(address):
32 ''' If we see an @-symbol in the address then check against the full
33 RFC syntax. Otherwise it is a local-only address so only check
34 the local part of the RFC syntax.
35 '''
36 if '@' in address:
37 return email_rfc.match(address)
38 else:
39 return email_local.match(address)
41 def get_addresses(user):
42 ''' iterate over all known addresses in a newvalues dict
43 this takes of the address/alterate_addresses handling
44 '''
45 if user.has_key('address'):
46 yield user['address']
47 if user.get('alternate_addresses', None):
48 for address in user['alternate_addresses'].split('\n'):
49 yield address
51 def audit_user_fields(db, cl, nodeid, newvalues):
52 ''' Make sure user properties are valid.
54 - email address is syntactically valid
55 - email address is unique
56 - roles specified exist
57 - timezone is valid
58 '''
60 for address in get_addresses(newvalues):
61 if not valid_address(address):
62 raise ValueError, 'Email address syntax is invalid'
64 check_main = db.user.stringFind(address=address)
65 # make sure none of the alts are owned by anyone other than us (x!=nodeid)
66 check_alts = [x for x in db.user.filter(None, {'alternate_addresses' : address}) if x != nodeid]
67 if check_main or check_alts:
68 raise ValueError, 'Email address %s already in use' % address
70 for rolename in [r.lower().strip() for r in newvalues.get('roles', '').split(',')]:
71 if rolename and not db.security.role.has_key(rolename):
72 raise ValueError, 'Role "%s" does not exist'%rolename
74 tz = newvalues.get('timezone', None)
75 if tz:
76 # if they set a new timezone validate the timezone by attempting to
77 # use it before we store it to the db.
78 import roundup.date
79 import datetime
80 try:
81 TZ = roundup.date.get_timezone(tz)
82 dt = datetime.datetime.now()
83 local = TZ.localize(dt).utctimetuple()
84 except IOError:
85 raise ValueError, 'Timezone "%s" does not exist' % tz
86 except ValueError:
87 raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz
89 def init(db):
90 # fire before changes are made
91 db.user.audit('set', audit_user_fields)
92 db.user.audit('create', audit_user_fields)
94 # vim: sts=4 sw=4 et si