3
0
Fork 0
wuttaweb/src/wuttaweb/views/auth.py

293 lines
9.5 KiB
Python

# -*- coding: utf-8; -*-
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework 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.
#
# Wutta Framework 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
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Auth Views
"""
import colander
from wuttaweb.views import View
from wuttaweb.db import Session
from wuttaweb.auth import login_user, logout_user
from wuttaweb.forms import widgets
class AuthView(View):
"""
Auth views shared by all apps.
"""
def login(self, session=None):
"""
View for user login.
This view shows the login form, and handles its submission.
Upon successful login, user is redirected to home page.
* route: ``login``
* template: ``/auth/login.mako``
"""
model = self.app.model
session = session or Session()
auth = self.app.get_auth_handler()
# nb. redirect to /setup if no users exist
user = session.query(model.User).first()
if not user:
return self.redirect(self.request.route_url('setup'))
referrer = self.request.get_referrer()
# redirect if already logged in
if self.request.user:
self.request.session.flash(f"{self.request.user} is already logged in", 'error')
return self.redirect(referrer)
form = self.make_form(schema=self.login_make_schema(),
align_buttons_right=True,
show_button_cancel=False,
show_button_reset=True,
button_label_submit="Login",
button_icon_submit='user')
# validate basic form data (sanity check)
data = form.validate()
if data:
# truly validate user credentials
user = auth.authenticate_user(session, data['username'], data['password'])
if user:
# okay now they're truly logged in
headers = login_user(self.request, user)
return self.redirect(referrer, headers=headers)
else:
self.request.session.flash("Invalid user credentials", 'error')
return {
'index_title': self.app.get_title(),
'form': form,
# TODO
# 'referrer': referrer,
}
def login_make_schema(self):
schema = colander.Schema()
# nb. we must explicitly declare the widgets in order to also
# specify the ref attribute. this is needed for autofocus and
# keydown behavior for login form.
schema.add(colander.SchemaNode(
colander.String(),
name='username',
widget=widgets.TextInputWidget(attributes={
'ref': 'username',
})))
schema.add(colander.SchemaNode(
colander.String(),
name='password',
widget=widgets.PasswordWidget(attributes={
'ref': 'password',
})))
return schema
def logout(self):
"""
View for user logout.
This deletes/invalidates the current user session and then
redirects to the login page.
Note that a simple GET is sufficient; POST is not required.
* route: ``logout``
* template: n/a
"""
# truly logout the user
headers = logout_user(self.request)
# TODO
# # redirect to home page after logout, if so configured
# if self.config.get_bool('wuttaweb.home_after_logout', default=False):
# return self.redirect(self.request.route_url('home'), headers=headers)
# otherwise redirect to referrer, with 'login' page as fallback
# TODO: should call request.get_referrer()
# referrer = self.request.get_referrer(default=self.request.route_url('login'))
referrer = self.request.route_url('login')
return self.redirect(referrer, headers=headers)
def change_password(self):
"""
View allowing a user to change their own password.
This view shows a change-password form, and handles its
submission. If successful, user is redirected to home page.
If current user is not authenticated, no form is shown and
user is redirected to home page.
* route: ``change_password``
* template: ``/auth/change_password.mako``
"""
if not self.request.user:
return self.redirect(self.request.route_url('home'))
form = self.make_form(schema=self.change_password_make_schema(),
show_button_cancel=False,
show_button_reset=True)
data = form.validate()
if data:
auth = self.app.get_auth_handler()
auth.set_user_password(self.request.user, data['new_password'])
self.request.session.flash("Your password has been changed.")
# TODO: should use request.get_referrer() instead
referrer = self.request.route_url('home')
return self.redirect(referrer)
return {'index_title': str(self.request.user),
'form': form}
def change_password_make_schema(self):
schema = colander.Schema()
schema.add(colander.SchemaNode(
colander.String(),
name='current_password',
widget=widgets.PasswordWidget(),
validator=self.change_password_validate_current_password))
schema.add(colander.SchemaNode(
colander.String(),
name='new_password',
widget=widgets.CheckedPasswordWidget(),
validator=self.change_password_validate_new_password))
return schema
def change_password_validate_current_password(self, node, value):
auth = self.app.get_auth_handler()
user = self.request.user
if not auth.check_user_password(user, value):
node.raise_invalid("Current password is incorrect.")
def change_password_validate_new_password(self, node, value):
auth = self.app.get_auth_handler()
user = self.request.user
if auth.check_user_password(user, value):
node.raise_invalid("New password must be different from old password.")
def become_root(self):
"""
Elevate the current request to 'root' for full system access.
This is only allowed if current (authenticated) user is a
member of the Administrator role. Also note that GET is not
allowed for this view, only POST.
See also :meth:`stop_root()`.
"""
if self.request.method != 'POST':
raise self.forbidden()
if not self.request.is_admin:
raise self.forbidden()
self.request.session['is_root'] = True
self.request.session.flash("You have been elevated to 'root' and now have full system access")
url = self.request.get_referrer()
return self.redirect(url)
def stop_root(self):
"""
Lower the current request from 'root' back to normal access.
Also note that GET is not allowed for this view, only POST.
See also :meth:`become_root()`.
"""
if self.request.method != 'POST':
raise self.forbidden()
if not self.request.is_admin:
raise self.forbidden()
self.request.session['is_root'] = False
self.request.session.flash("Your normal system access has been restored")
url = self.request.get_referrer()
return self.redirect(url)
@classmethod
def defaults(cls, config):
cls._auth_defaults(config)
@classmethod
def _auth_defaults(cls, config):
# login
config.add_route('login', '/login')
config.add_view(cls, attr='login',
route_name='login',
renderer='/auth/login.mako')
# logout
config.add_route('logout', '/logout')
config.add_view(cls, attr='logout',
route_name='logout')
# change password
config.add_route('change_password', '/change-password')
config.add_view(cls, attr='change_password',
route_name='change_password',
renderer='/auth/change_password.mako')
# become root
config.add_route('become_root', '/root/yes',
request_method='POST')
config.add_view(cls, attr='become_root',
route_name='become_root')
# stop root
config.add_route('stop_root', '/root/no',
request_method='POST')
config.add_view(cls, attr='stop_root',
route_name='stop_root')
def defaults(config, **kwargs):
base = globals()
AuthView = kwargs.get('AuthView', base['AuthView'])
AuthView.defaults(config)
def includeme(config):
defaults(config)