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 $ 22 23 import re 24 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) 30 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) 40 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 50 51 def audit_user_fields(db, cl, nodeid, newvalues): 52 ''' Make sure user properties are valid. 53 54 - email address is syntactically valid 55 - email address is unique 56 - roles specified exist 57 - timezone is valid 58 ''' 59 60 for address in get_addresses(newvalues): 61 if not valid_address(address): 62 raise ValueError, 'Email address syntax is invalid' 63 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 69 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 73 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 88 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) 93 94 # vim: sts=4 sw=4 et si