Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/views/auth.py: 100%
101 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:24 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:24 -0500
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# wuttaweb -- Web App for Wutta Framework
5# Copyright © 2024 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Auth Views
25"""
27import colander
29from wuttaweb.views import View
30from wuttaweb.db import Session
31from wuttaweb.auth import login_user, logout_user
32from wuttaweb.forms import widgets
35class AuthView(View):
36 """
37 Auth views shared by all apps.
38 """
40 def login(self, session=None):
41 """
42 View for user login.
44 This view shows the login form, and handles its submission.
45 Upon successful login, user is redirected to home page.
47 * route: ``login``
48 * template: ``/auth/login.mako``
49 """
50 model = self.app.model
51 session = session or Session()
52 auth = self.app.get_auth_handler()
54 # nb. redirect to /setup if no users exist
55 user = session.query(model.User).first()
56 if not user:
57 return self.redirect(self.request.route_url('setup'))
59 referrer = self.request.get_referrer()
61 # redirect if already logged in
62 if self.request.user:
63 self.request.session.flash(f"{self.request.user} is already logged in", 'error')
64 return self.redirect(referrer)
66 form = self.make_form(schema=self.login_make_schema(),
67 align_buttons_right=True,
68 show_button_cancel=False,
69 show_button_reset=True,
70 button_label_submit="Login",
71 button_icon_submit='user')
73 # validate basic form data (sanity check)
74 data = form.validate()
75 if data:
77 # truly validate user credentials
78 user = auth.authenticate_user(session, data['username'], data['password'])
79 if user:
81 # okay now they're truly logged in
82 headers = login_user(self.request, user)
83 return self.redirect(referrer, headers=headers)
85 else:
86 self.request.session.flash("Invalid user credentials", 'error')
88 return {
89 'index_title': self.app.get_title(),
90 'form': form,
91 # TODO
92 # 'referrer': referrer,
93 }
95 def login_make_schema(self):
96 schema = colander.Schema()
98 # nb. we must explicitly declare the widgets in order to also
99 # specify the ref attribute. this is needed for autofocus and
100 # keydown behavior for login form.
102 schema.add(colander.SchemaNode(
103 colander.String(),
104 name='username',
105 widget=widgets.TextInputWidget(attributes={
106 'ref': 'username',
107 })))
109 schema.add(colander.SchemaNode(
110 colander.String(),
111 name='password',
112 widget=widgets.PasswordWidget(attributes={
113 'ref': 'password',
114 })))
116 return schema
118 def logout(self):
119 """
120 View for user logout.
122 This deletes/invalidates the current user session and then
123 redirects to the login page.
125 Note that a simple GET is sufficient; POST is not required.
127 * route: ``logout``
128 * template: n/a
129 """
130 # truly logout the user
131 headers = logout_user(self.request)
133 # TODO
134 # # redirect to home page after logout, if so configured
135 # if self.config.get_bool('wuttaweb.home_after_logout', default=False):
136 # return self.redirect(self.request.route_url('home'), headers=headers)
138 # otherwise redirect to referrer, with 'login' page as fallback
139 # TODO: should call request.get_referrer()
140 # referrer = self.request.get_referrer(default=self.request.route_url('login'))
141 referrer = self.request.route_url('login')
142 return self.redirect(referrer, headers=headers)
144 def change_password(self):
145 """
146 View allowing a user to change their own password.
148 This view shows a change-password form, and handles its
149 submission. If successful, user is redirected to home page.
151 If current user is not authenticated, no form is shown and
152 user is redirected to home page.
154 * route: ``change_password``
155 * template: ``/auth/change_password.mako``
156 """
157 if not self.request.user:
158 return self.redirect(self.request.route_url('home'))
160 form = self.make_form(schema=self.change_password_make_schema(),
161 show_button_cancel=False,
162 show_button_reset=True)
164 data = form.validate()
165 if data:
166 auth = self.app.get_auth_handler()
167 auth.set_user_password(self.request.user, data['new_password'])
168 self.request.session.flash("Your password has been changed.")
169 # TODO: should use request.get_referrer() instead
170 referrer = self.request.route_url('home')
171 return self.redirect(referrer)
173 return {'index_title': str(self.request.user),
174 'form': form}
176 def change_password_make_schema(self):
177 schema = colander.Schema()
179 schema.add(colander.SchemaNode(
180 colander.String(),
181 name='current_password',
182 widget=widgets.PasswordWidget(),
183 validator=self.change_password_validate_current_password))
185 schema.add(colander.SchemaNode(
186 colander.String(),
187 name='new_password',
188 widget=widgets.CheckedPasswordWidget(),
189 validator=self.change_password_validate_new_password))
191 return schema
193 def change_password_validate_current_password(self, node, value):
194 auth = self.app.get_auth_handler()
195 user = self.request.user
196 if not auth.check_user_password(user, value):
197 node.raise_invalid("Current password is incorrect.")
199 def change_password_validate_new_password(self, node, value):
200 auth = self.app.get_auth_handler()
201 user = self.request.user
202 if auth.check_user_password(user, value):
203 node.raise_invalid("New password must be different from old password.")
205 def become_root(self):
206 """
207 Elevate the current request to 'root' for full system access.
209 This is only allowed if current (authenticated) user is a
210 member of the Administrator role. Also note that GET is not
211 allowed for this view, only POST.
213 See also :meth:`stop_root()`.
214 """
215 if self.request.method != 'POST':
216 raise self.forbidden()
218 if not self.request.is_admin:
219 raise self.forbidden()
221 self.request.session['is_root'] = True
222 self.request.session.flash("You have been elevated to 'root' and now have full system access")
224 url = self.request.get_referrer()
225 return self.redirect(url)
227 def stop_root(self):
228 """
229 Lower the current request from 'root' back to normal access.
231 Also note that GET is not allowed for this view, only POST.
233 See also :meth:`become_root()`.
234 """
235 if self.request.method != 'POST':
236 raise self.forbidden()
238 if not self.request.is_admin:
239 raise self.forbidden()
241 self.request.session['is_root'] = False
242 self.request.session.flash("Your normal system access has been restored")
244 url = self.request.get_referrer()
245 return self.redirect(url)
247 @classmethod
248 def defaults(cls, config):
249 cls._auth_defaults(config)
251 @classmethod
252 def _auth_defaults(cls, config):
254 # login
255 config.add_route('login', '/login')
256 config.add_view(cls, attr='login',
257 route_name='login',
258 renderer='/auth/login.mako')
260 # logout
261 config.add_route('logout', '/logout')
262 config.add_view(cls, attr='logout',
263 route_name='logout')
265 # change password
266 config.add_route('change_password', '/change-password')
267 config.add_view(cls, attr='change_password',
268 route_name='change_password',
269 renderer='/auth/change_password.mako')
271 # become root
272 config.add_route('become_root', '/root/yes',
273 request_method='POST')
274 config.add_view(cls, attr='become_root',
275 route_name='become_root')
277 # stop root
278 config.add_route('stop_root', '/root/no',
279 request_method='POST')
280 config.add_view(cls, attr='stop_root',
281 route_name='stop_root')
284def defaults(config, **kwargs):
285 base = globals()
287 AuthView = kwargs.get('AuthView', base['AuthView'])
288 AuthView.defaults(config)
291def includeme(config):
292 defaults(config)