async def change_password(document): decoded_token = document['authToken'] current_password = document['currentPassword'] new_password = document['newPassword'] user = await conf.mongo_accounts.find_one( filter = {'authTokens': {'$elemMatch': {'authToken': decoded_token}}}, projection = ['_id', 'password'] ) if not bcrypt.verify(current_password, user['password']): return Response( { "success": False, "error": _("Incorrect password.") }, 403 ) if bcrypt.verify(new_password, user['password']): return Response({ "success": False, "error": _("This is the same password as the current one.") }) hashed_new_password = bcrypt.hash(new_password) await conf.mongo_accounts.update_one( filter = {'_id': user['_id']}, update = {'$set': {'password': hashed_new_password}} ) return Response({"success": True})
async def _can_change_email(user, new_email): if 'emailLower' not in user: return False, _("This user didn't register via email.") if user['emailLower'] == new_email.lower(): return False, _("This is already the email address of this account.") if await email_exists(new_email, case_sensitive = False): return False, _("This email address already belongs to a registered account.") return True, None
async def login(document): user = await conf.mongo_accounts.find_one( {'email': document['email']}, projection = ( _actual_fields_names(document.get('fields', [])) + ['authTokens', 'password', 'active'] ) ) if user: if 'password' not in user: return Response({"success": False, "error": _("Password is not set for this user.")}, 403) if 'active' not in user or user['active'] is not True: return Response({"success": False, "error": _("This user isn't active.")}, 403) try: password_matches = bcrypt.verify(document['password'], user['password']) except ValueError: # this should not happen if the hash has been correctly computed return Response( { "success": False, "error": _("The password verification failed because of a server side issue.") }, status = 500 ) else: if password_matches: dct = { "success": True } fields = document.get('fields') if fields: dct['user'] = _rec_objectids_to_str(_fields_document(user, fields)) dct['authToken'] = await _generate_auth_token(user) return Response(dct) else: return Response( { "success": False, "error": _("Incorrect password.") }, 403 ) else: return Response( { "success": False, "error": _("No such user.") }, 403 )
def send_registration_code(self, code, to, lang=None): self.send_email( self.code_email(_('Registration'), self.REGISTRATION_CODE_MESSAGE, code, to, lang=lang), to)
def send_change_email_code(self, code, to, lang=None): self.send_email( self.code_email(_('Validation of your new email address'), self.CHANGE_EMAIL_CODE_MESSAGE, code, to, lang=lang), to)
async def change_email(document): decoded_token = document['authToken'] user_id = document['decoded']['_id'] new_email = document['decoded']['newEmail'] new_email_lower = new_email.lower() try: update_result = await conf.mongo_accounts.update_one( filter = { '_id': user_id, 'authTokens': { '$elemMatch': {'authToken': decoded_token} } }, update = {'$set': { 'emailLower': new_email_lower, 'email': new_email }} ) except DuplicateKeyError: return Response(email_used_result) else: if update_result.matched_count == 1: return Response({"success": True}) else: return Response( { "success": False, "error": _("No user account matches this user id and this authToken.") }, 403 )
def send_reset_password_code(self, code, to, lang=None): self.send_email( self.code_email(_('Password reset'), self.RESET_PASSWORD_CODE_MESSAGE, code, to, lang=lang), to)
async def delete_user(document): delete_result = await conf.mongo_accounts.delete_one( {'_id': document['_id']}, ) if delete_result.deleted_count == 1: return Response({"success": True}) else: return Response({"success": False, "error": _("No such user.")})
def __init__(self, regex, flags = 0, error_message = _("Incorrect value.")): if isinstance(regex, basestring): if not regex.startswith('^'): regex = '^' + regex if not regex.endswith('$'): regex = regex + '$' self.regex = re.compile(regex, flags) else: self.regex = regex self.error_message = error_message
async def check_reset_password_code(document): if await email_exists(document['decoded']['email'], case_sensitive = True): return Response({"success": True}) else: # The user could have changed their email address in the meantime. # That would be strange but not impossible. return Response({ "success": False, "error": _("No such email in the database.") })
def run(self, value): result = [] for i, val in enumerate(value): try: result.append(self._filter.run(val)) except ValidationError as exc: raise ValidationError( _("Item #%s: ") % (i + self.__class__.ITEM_START) + exc.error_details ) if isinstance(value, (tuple, set)): result = type(value)(result) return result
async def _set_active(user_id, value): update = {'$set': {'active': value}} if not value: update['$unset'] = {'authTokens': ''} update_result = await conf.mongo_accounts.update_one( filter = {'_id': user_id}, update = update ) if update_result.matched_count == 1: return Response({"success": True}) else: return Response({"success": False, "error": _("No such user.")})
async def del_user_fields(document): if document['del']: decoded_token = document['authToken'] update_result = await conf.mongo_accounts.update_one( filter = {'authTokens': {'$elemMatch': {'authToken': decoded_token}}}, update = {'$unset': {('fields.' + k): '' for k in document['del']}} ) if update_result.matched_count == 0: return Response( {"success": False, "error": _("Unrecognized authToken.")}, 403 ) return Response({"success": True})
async def check_change_email_code(document): user = await conf.mongo_accounts.find_one( filter = {'_id': document['decoded']['_id']}, projection = ['_id', 'emailLower'] ) if user: can_change, error = await _can_change_email(user, document['decoded']['newEmail']) if can_change: return Response({"success": True}) else: return Response({"success": False, "error": error}) else: return Response({"success": False, "error": _("No such user.")})
async def reset_password(document): email = document['decoded']['email'] new_password = document['newPassword'] update_result = await conf.mongo_accounts.update_one( filter = {'email': email}, update = {'$set': {'password': new_password}} ) if update_result.matched_count: return Response({"success": True}) else: return Response({ "success": False, "error": _("No such email in the database.") })
def run(self, value): type_ = type(value) if ( (self._subclasses and not (any (issubclass(type_, t) for t in self.types))) or (not self._subclasses and type_ not in self.types) ): types_str = ', '.join(t.__name__ for t in self.types) if len(self.types) == 1: raise ValidationError( _("Wrong type. Expected {type}. Got {wrong_type} instead.").format( type = types_str, wrong_type = type_.__name__ ) ) else: raise ValidationError( _("Wrong type. Expected one of {types}. Got {wrong_type} instead.").format( types = types_str, wrong_type = type_.__name__ ) ) return value
async def send_reset_password_code(document, lang): email = document['email'] if await email_exists(email, case_sensitive = True): reset_password_code = create_signed_document({ 'email': email, 'action': 'resetPassword' }) mailer.send_reset_password_code( reset_password_code, to = email, lang = lang ) return Response({"success": True}) else: return Response({"success": False, "error": _("No such email in the database.")})
async def get_user_fields(document): decoded_token = document['authToken'] user_document = await conf.mongo_accounts.find_one( {'authTokens': {'$elemMatch': {'authToken': decoded_token}}}, projection = _actual_fields_names(document['fields']) ) if user_document is None: return Response( {"success": False, "error": _("Unrecognized authToken.")}, 403 ) return Response({ "success": True, "user": _rec_objectids_to_str(_fields_document(user_document, document['fields'])) })
async def get_user_id(document): # TODO document in the readthedoc decoded_token = document['authToken'] user_document = await conf.mongo_accounts.find_one( {'authTokens': {'$elemMatch': {'authToken': decoded_token}}}, projection = ['_id'] ) if user_document is None: return Response( {"success": False, "error": _("Unrecognized authToken.")}, 403 ) else: return Response( {"success": True, "_id": str(user_document['_id'])} )
async def send_change_email_code(document, lang): decoded_token = document['authToken'] new_email = document['email'] user = await conf.mongo_accounts.find_one( filter = {'authTokens': {'$elemMatch': {'authToken': decoded_token}}}, projection = ['_id', 'emailLower'] ) if user: can_change, error = await _can_change_email(user, new_email) if can_change: change_email_code = create_signed_document({ 'action': 'changeEmail', '_id': str(user['_id']), 'newEmail': new_email }) mailer.send_change_email_code(change_email_code, new_email, lang = lang) return Response({"success": True}) else: return Response({"success": False, "error": error}) else: return Response( {"success": False, "error": _("Unrecognized authToken.")}, 403 )
validated_document = self.run(clear_document) except ValidationError as exc: if clear_document.get('authToken') and 'authToken' in exc.error_details: error_status = 403 else: error_status = 400 return Response( {self.success_key: False, "error": exc.error_details}, status = error_status ) else: return await func(validated_document, *args, **kwargs) return new_func email_used_result = { "success": False, "error": _("This email address already belongs to a registered account.") } async def email_exists(email, case_sensitive): if case_sensitive: filter = {"email": email} else: filter = {"emailLower": email.lower()} result = await conf.mongo_accounts.find_one(filter) return bool(result) @Sch(['email', Email]) async def send_registration_code(document, lang): email = document['email'] if await email_exists(email, case_sensitive = False): return Response(email_used_result)
def run(self, dict_): Type(dict, subclasses = True).run(dict_) dct = dict_.copy() errors = {} policy = self.unexpected_keys_policy if policy is not Schema.KEEP: for key in dict_: if key not in self.expected_fields: if policy is Schema.FAIL: errors[key] = _("Unexpected key {key}.").format(key = repr(key)) del dct[key] for chain in self.chains: if chain.field: field = chain.field[0] if field in dct: if dct[field] in chain.discard: del dct[field] try: value = dct[field] except KeyError: if chain.optional: continue if chain.default: if errors and isinstance(chain.default, DefaultFunc): continue # avoid working with potentially invalid data dct[field] = value = chain.default.getvalue(dct) else: errors[field] = _("Field is missing.") continue else: # we work on the whole document if errors: continue # avoid working with potentially invalid data value = dct # applying filters error = False for f in chain.filters: try: value = f.run(value) except ValidationError as exc: if chain.field: errors[chain.field[0]] = exc.error_details else: errors['*'] = exc.error_details error = True break if error: if isinstance(chain.storage_instruction, (SaveAs, MoveTo)): errors[chain.storage_instruction.name] = _("Couldn't compute field.") continue if chain.storage_instruction: if not chain.field and chain.storage_instruction is Save: dct = value else: chain.storage_instruction.execute(dct, chain.field[0] if chain.field else None, value) if errors: raise ValidationError(errors) return dct
def __init__(self, unary_test, error_message = _("Incorrect value.")): self.unary_test = unary_test self.error_message = error_message
def __init__(self, collection, error_message = _("Incorrect value.")): self.collection = collection self.error_message = error_message
if not regex.startswith('^'): regex = '^' + regex if not regex.endswith('$'): regex = regex + '$' self.regex = re.compile(regex, flags) else: self.regex = regex self.error_message = error_message def run(self, value): if not self.regex.match(value): raise ValidationError(self.error_message) return value def to_filter(f): if isinstance(f, Filter): return f elif f is int: return ToInt # to get the i18ned error messages elif f is float: return ToFloat # same as above elif callable(f): return Apply(f) elif hasattr(f, '__contains__'): return In(f) else: raise ValueError("%s is not a valid filter" % repr(f)) ToInt = Apply(int, error_message = _("This should be an integer.")) # useful to get i18ned error messages ToFloat = Apply(float, error_message = _("This should be a number."))
def DecodeSignedString(max_age_days, error_message=None): return Do( Apply(lambda message: decode_signed_string(message, max_age_days)), Assert(lambda result: result is not None), error_message=error_message or _("Invalid or expired code"))
class Mailer(object): REGISTRATION_CODE_MESSAGE = _("""Hello, please follow this link to confirm your email address and register at {serviceName}: {url} See you in a minute! """) RESET_PASSWORD_CODE_MESSAGE = _("""Hello, you asked to reset your password at {serviceName}. To set a new password, follow this link: {url} """) CHANGE_EMAIL_CODE_MESSAGE = _("""Hello, you asked to change the email address associated with your account. Please follow this link to confirm your new email address: {url} """) def __init__(self): self.serviceName = conf.get('serviceName') self.emailCodeLink = conf.get('emailCodeLink') smtp_params = conf.get('smtp') self.email_account = EmailAccount( smtp_params['from'], smtp_params['smtpServer'], smtp_params['username'], smtp_params['password'], port=smtp_params['port'], sender_name=smtp_params.get('senderName')) def code_email(self, subject, template, code, to, lang=None): msg = MIMEText( translate( template.format(serviceName=self.serviceName, url=url_add_element(self.emailCodeLink, code)), lang or 'en')) msg['From'] = self.email_account.email msg['To'] = to msg['Date'] = formatdate(localtime=True) msg['Subject'] = '[%s] %s' % (self.serviceName, translate(subject, lang or 'en')) return msg def send_email(self, email, to): IOLoop.current().spawn_callback(self.email_account.send_email, email, to) def send_registration_code(self, code, to, lang=None): self.send_email( self.code_email(_('Registration'), self.REGISTRATION_CODE_MESSAGE, code, to, lang=lang), to) def send_reset_password_code(self, code, to, lang=None): self.send_email( self.code_email(_('Password reset'), self.RESET_PASSWORD_CODE_MESSAGE, code, to, lang=lang), to) def send_change_email_code(self, code, to, lang=None): self.send_email( self.code_email(_('Validation of your new email address'), self.CHANGE_EMAIL_CODE_MESSAGE, code, to, lang=lang), to)
from coolsignup.conf import conf from naval import Type, Length, Assert, Do from postpone import LazyString as _ from zxcvbn import zxcvbn PASSWORD_MAX_LENGTH = 800 NewPasswordValidator = Do( Type(str), Length(conf.get('passwordMinLength'), PASSWORD_MAX_LENGTH), Assert( (lambda p: zxcvbn(p)['guesses_log10'] >= conf.get('passwordStrength')), error_message=_("Password too easy to guess.")))
from validators import email, ValidationFailure from naval.core import * from postpone import LazyString as _ __all__ = ['Email', 'Url'] Email = Assert( lambda v: not isinstance(email(v, whitelist = ()), ValidationFailure), error_message = _("This is not a valid email address.") ) Email.__doc__ = """ Email validator. This validator uses the email validator from the "validators" library: https://github.com/kvesteri/validators """ Url = Do( Type(str), Length(max=2083), # regex stolen from the php Spoon Library: https://github.com/spoon/library/blob/master/spoon/filter/filter.php Regex( r'(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\xa1-\xff0-9]+-?)*[a-z\xa1-\xff0-9]+)(?:\.(?:[a-z\xa1}-\xff0-9]+-?)*[a-z\xa1-\xff0-9]+)*(?:\.(?:[a-z\xa1-\xff]{2,})))(?::\d{2,5})?(?:/[^\s]*)?' ), error_message = _("This is not a valid url.") ) Url.__doc__ = """ Url validator. The regex used is stolen from the php Spoon Library: https://github.com/spoon/library/blob/master/spoon/filter/filter.php """