login.py
255 lines
| 10.7 KiB
| text/x-python
|
PythonLexer
Bradley M. Kuhn
|
r4187 | # -*- coding: utf-8 -*- | ||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU General Public License as published by | ||||
# the Free Software Foundation, either version 3 of the License, or | ||||
# (at your option) any later version. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
""" | ||||
kallithea.controllers.login | ||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
Bradley M. Kuhn
|
r4212 | Login controller for Kallithea | ||
Bradley M. Kuhn
|
r4187 | |||
Bradley M. Kuhn
|
r4211 | This file was forked by the Kallithea project in July 2014. | ||
Original author and date, and relevant copyright and licensing information is below: | ||||
Bradley M. Kuhn
|
r4187 | :created_on: Apr 22, 2010 | ||
:author: marcink | ||||
Bradley M. Kuhn
|
r4211 | :copyright: (c) 2013 RhodeCode GmbH, and others. | ||
Bradley M. Kuhn
|
r4208 | :license: GPLv3, see LICENSE.md for more details. | ||
Bradley M. Kuhn
|
r4187 | """ | ||
import logging | ||||
Søren Løvborg
|
r5523 | import re | ||
Mads Kiilerich
|
r7718 | |||
Bradley M. Kuhn
|
r4187 | import formencode | ||
from formencode import htmlfill | ||||
Mads Kiilerich
|
r7718 | from tg import request, session | ||
from tg import tmpl_context as c | ||||
Mads Kiilerich
|
r6508 | from tg.i18n import ugettext as _ | ||
Mads Kiilerich
|
r7718 | from webob.exc import HTTPBadRequest, HTTPFound | ||
Bradley M. Kuhn
|
r4187 | |||
import kallithea.lib.helpers as h | ||||
Thomas De Schampheleire
|
r6182 | from kallithea.config.routing import url | ||
Bradley M. Kuhn
|
r4187 | from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator | ||
Søren Løvborg
|
r5256 | from kallithea.lib.base import BaseController, log_in_user, render | ||
Bradley M. Kuhn
|
r4187 | from kallithea.lib.exceptions import UserCreationError | ||
Mads Kiilerich
|
r7718 | from kallithea.model.db import Setting, User | ||
from kallithea.model.forms import LoginForm, PasswordResetConfirmationForm, PasswordResetRequestForm, RegisterForm | ||||
from kallithea.model.meta import Session | ||||
Bradley M. Kuhn
|
r4187 | from kallithea.model.user import UserModel | ||
log = logging.getLogger(__name__) | ||||
class LoginController(BaseController): | ||||
Søren Løvborg
|
r5523 | def _validate_came_from(self, came_from, | ||
_re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")): | ||||
"""Return True if came_from is valid and can and should be used. | ||||
Determines if a URI reference is valid and relative to the origin; | ||||
or in RFC 3986 terms, whether it matches this production: | ||||
origin-relative-ref = path-absolute [ "?" query ] [ "#" fragment ] | ||||
with the exception that '%' escapes are not validated and '#' is | ||||
allowed inside the fragment part. | ||||
""" | ||||
return _re.match(came_from) is not None | ||||
Bradley M. Kuhn
|
r4187 | |||
def index(self): | ||||
Mads Kiilerich
|
r8076 | c.came_from = request.GET.get('came_from', '') | ||
Mads Kiilerich
|
r5509 | if c.came_from: | ||
if not self._validate_came_from(c.came_from): | ||||
log.error('Invalid came_from (not server-relative): %r', c.came_from) | ||||
raise HTTPBadRequest() | ||||
Mads Kiilerich
|
r5508 | else: | ||
Søren Løvborg
|
r5512 | c.came_from = url('home') | ||
Bradley M. Kuhn
|
r4187 | |||
if request.POST: | ||||
# import Login Form validator class | ||||
Thomas De Schampheleire
|
r6325 | login_form = LoginForm()() | ||
Bradley M. Kuhn
|
r4187 | try: | ||
c.form_result = login_form.to_python(dict(request.POST)) | ||||
# form checks for username/password, now we're authenticated | ||||
Søren Løvborg
|
r5256 | username = c.form_result['username'] | ||
Andrew Shadura
|
r5671 | user = User.get_by_username_or_email(username, case_insensitive=True) | ||
Mads Kiilerich
|
r5374 | except formencode.Invalid as errors: | ||
Bradley M. Kuhn
|
r4187 | defaults = errors.value | ||
# remove password from filling in form again | ||||
Mads Kiilerich
|
r5846 | defaults.pop('password', None) | ||
Bradley M. Kuhn
|
r4187 | return htmlfill.render( | ||
render('/login.html'), | ||||
defaults=errors.value, | ||||
errors=errors.error_dict or {}, | ||||
prefix_error=False, | ||||
Mads Kiilerich
|
r4941 | encoding="UTF-8", | ||
force_defaults=False) | ||||
Mads Kiilerich
|
r5374 | except UserCreationError as e: | ||
Bradley M. Kuhn
|
r4187 | # container auth or other auth functions that create users on | ||
# the fly can throw this exception signaling that there's issue | ||||
# with user creation, explanation should be provided in | ||||
# Exception itself | ||||
h.flash(e, 'error') | ||||
Søren Løvborg
|
r5256 | else: | ||
Mads Kiilerich
|
r7603 | auth_user = log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr) | ||
Mads Kiilerich
|
r7602 | # TODO: handle auth_user is None as failed authentication? | ||
Søren Løvborg
|
r5512 | raise HTTPFound(location=c.came_from) | ||
Mads Kiilerich
|
r7424 | else: | ||
# redirect if already logged in | ||||
Mads Kiilerich
|
r7599 | if not request.authuser.is_anonymous: | ||
Mads Kiilerich
|
r7424 | raise HTTPFound(location=c.came_from) | ||
Mads Kiilerich
|
r7599 | # continue to show login to default user | ||
Bradley M. Kuhn
|
r4187 | |||
return render('/login.html') | ||||
@HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate', | ||||
'hg.register.manual_activate') | ||||
def register(self): | ||||
Mads Kiilerich
|
r7403 | def_user_perms = AuthUser(dbuser=User.get_default_user()).permissions['global'] | ||
c.auto_active = 'hg.register.auto_activate' in def_user_perms | ||||
Bradley M. Kuhn
|
r4187 | |||
Bradley M. Kuhn
|
r4203 | settings = Setting.get_app_settings() | ||
Bradley M. Kuhn
|
r4218 | captcha_private_key = settings.get('captcha_private_key') | ||
Bradley M. Kuhn
|
r4187 | c.captcha_active = bool(captcha_private_key) | ||
Bradley M. Kuhn
|
r4218 | c.captcha_public_key = settings.get('captcha_public_key') | ||
Bradley M. Kuhn
|
r4187 | |||
if request.POST: | ||||
register_form = RegisterForm()() | ||||
try: | ||||
form_result = register_form.to_python(dict(request.POST)) | ||||
form_result['active'] = c.auto_active | ||||
if c.captcha_active: | ||||
from kallithea.lib.recaptcha import submit | ||||
Patrick Vane
|
r7170 | response = submit(request.POST.get('g-recaptcha-response'), | ||
Bradley M. Kuhn
|
r4187 | private_key=captcha_private_key, | ||
Mads Kiilerich
|
r6412 | remoteip=request.ip_addr) | ||
Patrick Vane
|
r7170 | if not response.is_valid: | ||
Bradley M. Kuhn
|
r4187 | _value = form_result | ||
Mads Kiilerich
|
r5127 | _msg = _('Bad captcha') | ||
Bradley M. Kuhn
|
r4187 | error_dict = {'recaptcha_field': _msg} | ||
raise formencode.Invalid(_msg, _value, None, | ||||
error_dict=error_dict) | ||||
UserModel().create_registration(form_result) | ||||
Søren Løvborg
|
r6030 | h.flash(_('You have successfully registered with %s') % (c.site_name or 'Kallithea'), | ||
Bradley M. Kuhn
|
r4187 | category='success') | ||
Session().commit() | ||||
Søren Løvborg
|
r5543 | raise HTTPFound(location=url('login_home')) | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r5374 | except formencode.Invalid as errors: | ||
Bradley M. Kuhn
|
r4187 | return htmlfill.render( | ||
render('/register.html'), | ||||
defaults=errors.value, | ||||
errors=errors.error_dict or {}, | ||||
prefix_error=False, | ||||
Mads Kiilerich
|
r4941 | encoding="UTF-8", | ||
force_defaults=False) | ||||
Mads Kiilerich
|
r5374 | except UserCreationError as e: | ||
Bradley M. Kuhn
|
r4187 | # container auth or other auth functions that create users on | ||
# the fly can throw this exception signaling that there's issue | ||||
# with user creation, explanation should be provided in | ||||
# Exception itself | ||||
h.flash(e, 'error') | ||||
return render('/register.html') | ||||
def password_reset(self): | ||||
Bradley M. Kuhn
|
r4203 | settings = Setting.get_app_settings() | ||
Bradley M. Kuhn
|
r4218 | captcha_private_key = settings.get('captcha_private_key') | ||
Bradley M. Kuhn
|
r4187 | c.captcha_active = bool(captcha_private_key) | ||
Bradley M. Kuhn
|
r4218 | c.captcha_public_key = settings.get('captcha_public_key') | ||
Bradley M. Kuhn
|
r4187 | |||
if request.POST: | ||||
Andrew Shadura
|
r5457 | password_reset_form = PasswordResetRequestForm()() | ||
Bradley M. Kuhn
|
r4187 | try: | ||
form_result = password_reset_form.to_python(dict(request.POST)) | ||||
if c.captcha_active: | ||||
from kallithea.lib.recaptcha import submit | ||||
Patrick Vane
|
r7170 | response = submit(request.POST.get('g-recaptcha-response'), | ||
Bradley M. Kuhn
|
r4187 | private_key=captcha_private_key, | ||
Mads Kiilerich
|
r6412 | remoteip=request.ip_addr) | ||
Patrick Vane
|
r7170 | if not response.is_valid: | ||
Bradley M. Kuhn
|
r4187 | _value = form_result | ||
Mads Kiilerich
|
r5127 | _msg = _('Bad captcha') | ||
Bradley M. Kuhn
|
r4187 | error_dict = {'recaptcha_field': _msg} | ||
raise formencode.Invalid(_msg, _value, None, | ||||
error_dict=error_dict) | ||||
Andrew Shadura
|
r5457 | redirect_link = UserModel().send_reset_password_email(form_result) | ||
h.flash(_('A password reset confirmation code has been sent'), | ||||
Bradley M. Kuhn
|
r4187 | category='success') | ||
Søren Løvborg
|
r5543 | raise HTTPFound(location=redirect_link) | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r5374 | except formencode.Invalid as errors: | ||
Bradley M. Kuhn
|
r4187 | return htmlfill.render( | ||
render('/password_reset.html'), | ||||
defaults=errors.value, | ||||
errors=errors.error_dict or {}, | ||||
prefix_error=False, | ||||
Mads Kiilerich
|
r4941 | encoding="UTF-8", | ||
force_defaults=False) | ||||
Bradley M. Kuhn
|
r4187 | |||
return render('/password_reset.html') | ||||
def password_reset_confirmation(self): | ||||
Andrew Shadura
|
r5457 | # This controller handles both GET and POST requests, though we | ||
# only ever perform the actual password change on POST (since | ||||
# GET requests are not allowed to have side effects, and do not | ||||
# receive automatic CSRF protection). | ||||
# The template needs the email address outside of the form. | ||||
c.email = request.params.get('email') | ||||
Mads Kiilerich
|
r8010 | c.timestamp = request.params.get('timestamp') or '' | ||
c.token = request.params.get('token') or '' | ||||
Andrew Shadura
|
r5457 | if not request.POST: | ||
Mads Kiilerich
|
r8010 | return render('/password_reset_confirmation.html') | ||
Bradley M. Kuhn
|
r4187 | |||
Andrew Shadura
|
r5457 | form = PasswordResetConfirmationForm()() | ||
try: | ||||
form_result = form.to_python(dict(request.POST)) | ||||
except formencode.Invalid as errors: | ||||
return htmlfill.render( | ||||
render('/password_reset_confirmation.html'), | ||||
defaults=errors.value, | ||||
errors=errors.error_dict or {}, | ||||
prefix_error=False, | ||||
encoding='UTF-8') | ||||
if not UserModel().verify_reset_password_token( | ||||
form_result['email'], | ||||
form_result['timestamp'], | ||||
form_result['token'], | ||||
): | ||||
return htmlfill.render( | ||||
render('/password_reset_confirmation.html'), | ||||
defaults=form_result, | ||||
errors={'token': _('Invalid password reset token')}, | ||||
prefix_error=False, | ||||
encoding='UTF-8') | ||||
UserModel().reset_password(form_result['email'], form_result['password']) | ||||
h.flash(_('Successfully updated password'), category='success') | ||||
Søren Løvborg
|
r5543 | raise HTTPFound(location=url('login_home')) | ||
Bradley M. Kuhn
|
r4187 | |||
def logout(self): | ||||
session.delete() | ||||
log.info('Logging out and deleting session for user') | ||||
Søren Løvborg
|
r5543 | raise HTTPFound(location=url('home')) | ||
Mads Kiilerich
|
r4993 | |||
Mads Kiilerich
|
r7711 | def session_csrf_secret_token(self): | ||
Mads Kiilerich
|
r4993 | """Return the CSRF protection token for the session - just like it | ||
Søren Løvborg
|
r5316 | could have been screen scraped from a page with a form. | ||
Mads Kiilerich
|
r4993 | Only intended for testing but might also be useful for other kinds | ||
of automation. | ||||
""" | ||||
Mads Kiilerich
|
r7709 | return h.session_csrf_secret_token() | ||