diff options
Diffstat (limited to 'mediagoblin/auth')
-rw-r--r-- | mediagoblin/auth/forms.py | 56 | ||||
-rw-r--r-- | mediagoblin/auth/lib.py | 4 | ||||
-rw-r--r-- | mediagoblin/auth/views.py | 197 |
3 files changed, 141 insertions, 116 deletions
diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index 0b2bf959..33e1f45c 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -15,54 +15,76 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import wtforms -import re -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.mail import normalize_email +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ + +def normalize_user_or_email_field(allow_email=True, allow_user=True): + """Check if we were passed a field that matches a username and/or email pattern + + This is useful for fields that can take either a username or email + address. Use the parameters if you want to only allow a username for + instance""" + message = _(u'Invalid User name or email address.') + nomail_msg = _(u"This field does not take email addresses.") + nouser_msg = _(u"This field requires an email address.") + + def _normalize_field(form, field): + email = u'@' in field.data + if email: # normalize email address casing + if not allow_email: + raise wtforms.ValidationError(nomail_msg) + wtforms.validators.Email()(form, field) + field.data = normalize_email(field.data) + else: # lower case user names + if not allow_user: + raise wtforms.ValidationError(nouser_msg) + wtforms.validators.Length(min=3, max=30)(form, field) + wtforms.validators.Regexp(r'^\w+$')(form, field) + field.data = field.data.lower() + if field.data is None: # should not happen, but be cautious anyway + raise wtforms.ValidationError(message) + return _normalize_field class RegistrationForm(wtforms.Form): username = wtforms.TextField( _('Username'), [wtforms.validators.Required(), - wtforms.validators.Length(min=3, max=30), - wtforms.validators.Regexp(r'^\w+$')]) + normalize_user_or_email_field(allow_email=False)]) password = wtforms.PasswordField( _('Password'), [wtforms.validators.Required(), - wtforms.validators.Length(min=6, max=30)]) + wtforms.validators.Length(min=5, max=1024)]) email = wtforms.TextField( _('Email address'), [wtforms.validators.Required(), - wtforms.validators.Email()]) + normalize_user_or_email_field(allow_user=False)]) class LoginForm(wtforms.Form): username = wtforms.TextField( - _('Username'), + _('Username or Email'), [wtforms.validators.Required(), - wtforms.validators.Regexp(r'^\w+$')]) + normalize_user_or_email_field()]) password = wtforms.PasswordField( _('Password'), - [wtforms.validators.Required()]) + [wtforms.validators.Required(), + wtforms.validators.Length(min=5, max=1024)]) class ForgotPassForm(wtforms.Form): username = wtforms.TextField( _('Username or email'), - [wtforms.validators.Required()]) - - def validate_username(form, field): - if not (re.match(r'^\w+$', field.data) or - re.match(r'^.+@[^.].*\.[a-z]{2,10}$', field.data, - re.IGNORECASE)): - raise wtforms.ValidationError(_(u'Incorrect input')) + [wtforms.validators.Required(), + normalize_user_or_email_field()]) class ChangePassForm(wtforms.Form): password = wtforms.PasswordField( 'Password', [wtforms.validators.Required(), - wtforms.validators.Length(min=6, max=30)]) + wtforms.validators.Length(min=5, max=1024)]) userid = wtforms.HiddenField( '', [wtforms.validators.Required()]) diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py index c5b046d2..8829995a 100644 --- a/mediagoblin/auth/lib.py +++ b/mediagoblin/auth/lib.py @@ -109,7 +109,7 @@ def send_verification_email(user, request): 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format( host=request.host, uri=request.urlgen('mediagoblin.auth.verify_email'), - userid=unicode(user._id), + userid=unicode(user.id), verification_key=user.verification_key)}) # TODO: There is no error handling in place @@ -144,7 +144,7 @@ def send_fp_verification_email(user, request): 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format( host=request.host, uri=request.urlgen('mediagoblin.auth.verify_forgot_password'), - userid=unicode(user._id), + userid=unicode(user.id), fp_verification_key=user.fp_verification_key)}) # TODO: There is no error handling in place diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 5b77c122..dc408911 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -17,18 +17,15 @@ import uuid import datetime -from webob import exc - -from mediagoblin import messages -from mediagoblin import mg_globals +from mediagoblin import messages, mg_globals +from mediagoblin.db.models import User from mediagoblin.tools.response import render_to_response, redirect, render_404 from mediagoblin.tools.translate import pass_to_ugettext as _ -from mediagoblin.db.util import ObjectId, InvalidId from mediagoblin.auth import lib as auth_lib from mediagoblin.auth import forms as auth_forms from mediagoblin.auth.lib import send_verification_email, \ send_fp_verification_email - +from sqlalchemy import or_ def email_debug_message(request): """ @@ -44,8 +41,10 @@ def email_debug_message(request): def register(request): - """ - Your classic registration view! + """The registration view. + + Note that usernames will always be lowercased. Email domains are lowercased while + the first part remains case-sensitive. """ # Redirects to indexpage if registrations are disabled if not mg_globals.app_config["allow_registration"]: @@ -59,14 +58,8 @@ def register(request): if request.method == 'POST' and register_form.validate(): # TODO: Make sure the user doesn't exist already - username = unicode(request.form['username'].lower()) - em_user, em_dom = unicode(request.form['email']).split("@", 1) - em_dom = em_dom.lower() - email = em_user + "@" + em_dom - users_with_username = request.db.User.find( - {'username': username}).count() - users_with_email = request.db.User.find( - {'email': email}).count() + users_with_username = User.query.filter_by(username=register_form.data['username']).count() + users_with_email = User.query.filter_by(email=register_form.data['email']).count() extra_validation_passes = True @@ -81,16 +74,16 @@ def register(request): if extra_validation_passes: # Create the user - user = request.db.User() - user.username = username - user.email = email + user = User() + user.username = register_form.data['username'] + user.email = register_form.data['email'] user.pw_hash = auth_lib.bcrypt_gen_password_hash( - request.form['password']) + register_form.password.data) user.verification_key = unicode(uuid.uuid4()) - user.save(validate=True) + user.save() # log the user in - request.session['user_id'] = unicode(user._id) + request.session['user_id'] = unicode(user.id) request.session.save() # send verification email @@ -119,21 +112,29 @@ def login(request): login_failed = False - if request.method == 'POST' and login_form.validate(): - user = request.db.User.find_one( - {'username': request.form['username'].lower()}) + if request.method == 'POST': + + username = login_form.data['username'] - if user and user.check_login(request.form['password']): - # set up login in session - request.session['user_id'] = unicode(user._id) - request.session.save() + if login_form.validate(): + user = User.query.filter( + or_( + User.username == username, + User.email == username, - if request.form.get('next'): - return exc.HTTPFound(location=request.form['next']) - else: - return redirect(request, "index") + )).first() - else: + if user and user.check_login(login_form.password.data): + # set up login in session + request.session['user_id'] = unicode(user.id) + request.session.save() + + if request.form.get('next'): + return redirect(request, location=request.form['next']) + else: + return redirect(request, "index") + + # Some failure during login occured if we are here! # Prevent detecting who's on this system by testing login # attempt timings auth_lib.fake_login_attempt() @@ -166,8 +167,7 @@ def verify_email(request): if not 'userid' in request.GET or not 'token' in request.GET: return render_404(request) - user = request.db.User.find_one( - {'_id': ObjectId(unicode(request.GET['userid']))}) + user = User.query.filter_by(id=request.args['userid']).first() if user and user.verification_key == unicode(request.GET['token']): user.status = u'active' @@ -204,7 +204,7 @@ def resend_activation(request): request, messages.ERROR, _('You must be logged in so we know who to send the email to!')) - + return redirect(request, 'mediagoblin.auth.login') if request.user.email_verified: @@ -212,12 +212,12 @@ def resend_activation(request): request, messages.ERROR, _("You've already verified your email address!")) - + return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username']) request.user.verification_key = unicode(uuid.uuid4()) request.user.save() - + email_debug_message(request) send_verification_email(request.user, request) @@ -234,61 +234,66 @@ def forgot_password(request): """ Forgot password view - Sends an email with an url to renew forgotten password + Sends an email with an url to renew forgotten password. + Use GET querystring parameter 'username' to pre-populate the input field """ fp_form = auth_forms.ForgotPassForm(request.form, - username=request.GET.get('username')) - - if request.method == 'POST' and fp_form.validate(): - - # '$or' not available till mongodb 1.5.3 - user = request.db.User.find_one( - {'username': request.form['username']}) - if not user: - user = request.db.User.find_one( - {'email': request.form['username']}) - - if user: - if user.email_verified and user.status == 'active': - user.fp_verification_key = unicode(uuid.uuid4()) - user.fp_token_expire = datetime.datetime.now() + \ - datetime.timedelta(days=10) - user.save() - - send_fp_verification_email(user, request) - - messages.add_message( - request, - messages.INFO, - _("An email has been sent with instructions on how to " - "change your password.")) - email_debug_message(request) - - else: - # special case... we can't send the email because the - # username is inactive / hasn't verified their email - messages.add_message( - request, - messages.WARNING, - _("Could not send password recovery email as " - "your username is inactive or your account's " - "email address has not been verified.")) - - return redirect( - request, 'mediagoblin.user_pages.user_home', - user=user.username) - return redirect(request, 'mediagoblin.auth.login') - else: - messages.add_message( - request, - messages.WARNING, - _("Couldn't find someone with that username or email.")) + username=request.args.get('username')) + + if not (request.method == 'POST' and fp_form.validate()): + # Either GET request, or invalid form submitted. Display the template + return render_to_response(request, + 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form}) + + # If we are here: method == POST and form is valid. username casing + # has been sanitized. Store if a user was found by email. We should + # not reveal if the operation was successful then as we don't want to + # leak if an email address exists in the system. + found_by_email = '@' in fp_form.username.data + + if found_by_email: + user = User.query.filter_by( + email = fp_form.username.data).first() + # Don't reveal success in case the lookup happened by email address. + success_message=_("If that email address (case sensitive!) is " + "registered an email has been sent with instructions " + "on how to change your password.") + + else: # found by username + user = User.query.filter_by( + username = fp_form.username.data).first() + + if user is None: + messages.add_message(request, + messages.WARNING, + _("Couldn't find someone with that username.")) return redirect(request, 'mediagoblin.auth.forgot_password') - return render_to_response( - request, - 'mediagoblin/auth/forgot_password.html', - {'fp_form': fp_form}) + success_message=_("An email has been sent with instructions " + "on how to change your password.") + + if user and not(user.email_verified and user.status == 'active'): + # Don't send reminder because user is inactive or has no verified email + messages.add_message(request, + messages.WARNING, + _("Could not send password recovery email as your username is in" + "active or your account's email address has not been verified.")) + + return redirect(request, 'mediagoblin.user_pages.user_home', + user=user.username) + + # SUCCESS. Send reminder and return to login page + if user: + user.fp_verification_key = unicode(uuid.uuid4()) + user.fp_token_expire = datetime.datetime.now() + \ + datetime.timedelta(days=10) + user.save() + + email_debug_message(request) + send_fp_verification_email(user, request) + + messages.add_message(request, messages.INFO, success_message) + return redirect(request, 'mediagoblin.auth.login') def verify_forgot_password(request): @@ -305,11 +310,9 @@ def verify_forgot_password(request): formdata_userid = formdata['vars']['userid'] formdata_vars = formdata['vars'] - # check if it's a valid Id - try: - user = request.db.User.find_one( - {'_id': ObjectId(unicode(formdata_userid))}) - except InvalidId: + # check if it's a valid user id + user = User.query.filter_by(id=formdata_userid).first() + if not user: return render_404(request) # check if we have a real user and correct token @@ -322,7 +325,7 @@ def verify_forgot_password(request): if request.method == 'POST' and cp_form.validate(): user.pw_hash = auth_lib.bcrypt_gen_password_hash( - request.form['password']) + cp_form.password.data) user.fp_verification_key = None user.fp_token_expire = None user.save() @@ -338,7 +341,7 @@ def verify_forgot_password(request): 'mediagoblin/auth/change_fp.html', {'cp_form': cp_form}) - # in case there is a valid id but no user whit that id in the db + # in case there is a valid id but no user with that id in the db # or the token expired else: return render_404(request) |