Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/views/auth.py: 100%
103 statements
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-28 21:19 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-28 21:19 -0600
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 if self.request.user.prevent_edit:
161 raise self.forbidden()
163 form = self.make_form(schema=self.change_password_make_schema(),
164 show_button_cancel=False,
165 show_button_reset=True)
167 data = form.validate()
168 if data:
169 auth = self.app.get_auth_handler()
170 auth.set_user_password(self.request.user, data['new_password'])
171 self.request.session.flash("Your password has been changed.")
172 # TODO: should use request.get_referrer() instead
173 referrer = self.request.route_url('home')
174 return self.redirect(referrer)
176 return {'index_title': str(self.request.user),
177 'form': form}
179 def change_password_make_schema(self):
180 schema = colander.Schema()
182 schema.add(colander.SchemaNode(
183 colander.String(),
184 name='current_password',
185 widget=widgets.PasswordWidget(),
186 validator=self.change_password_validate_current_password))
188 schema.add(colander.SchemaNode(
189 colander.String(),
190 name='new_password',
191 widget=widgets.CheckedPasswordWidget(),
192 validator=self.change_password_validate_new_password))
194 return schema
196 def change_password_validate_current_password(self, node, value):
197 auth = self.app.get_auth_handler()
198 user = self.request.user
199 if not auth.check_user_password(user, value):
200 node.raise_invalid("Current password is incorrect.")
202 def change_password_validate_new_password(self, node, value):
203 auth = self.app.get_auth_handler()
204 user = self.request.user
205 if auth.check_user_password(user, value):
206 node.raise_invalid("New password must be different from old password.")
208 def become_root(self):
209 """
210 Elevate the current request to 'root' for full system access.
212 This is only allowed if current (authenticated) user is a
213 member of the Administrator role. Also note that GET is not
214 allowed for this view, only POST.
216 See also :meth:`stop_root()`.
217 """
218 if self.request.method != 'POST':
219 raise self.forbidden()
221 if not self.request.is_admin:
222 raise self.forbidden()
224 self.request.session['is_root'] = True
225 self.request.session.flash("You have been elevated to 'root' and now have full system access")
227 url = self.request.get_referrer()
228 return self.redirect(url)
230 def stop_root(self):
231 """
232 Lower the current request from 'root' back to normal access.
234 Also note that GET is not allowed for this view, only POST.
236 See also :meth:`become_root()`.
237 """
238 if self.request.method != 'POST':
239 raise self.forbidden()
241 if not self.request.is_admin:
242 raise self.forbidden()
244 self.request.session['is_root'] = False
245 self.request.session.flash("Your normal system access has been restored")
247 url = self.request.get_referrer()
248 return self.redirect(url)
250 @classmethod
251 def defaults(cls, config):
252 cls._auth_defaults(config)
254 @classmethod
255 def _auth_defaults(cls, config):
257 # login
258 config.add_route('login', '/login')
259 config.add_view(cls, attr='login',
260 route_name='login',
261 renderer='/auth/login.mako')
263 # logout
264 config.add_route('logout', '/logout')
265 config.add_view(cls, attr='logout',
266 route_name='logout')
268 # change password
269 config.add_route('change_password', '/change-password')
270 config.add_view(cls, attr='change_password',
271 route_name='change_password',
272 renderer='/auth/change_password.mako')
274 # become root
275 config.add_route('become_root', '/root/yes',
276 request_method='POST')
277 config.add_view(cls, attr='become_root',
278 route_name='become_root')
280 # stop root
281 config.add_route('stop_root', '/root/no',
282 request_method='POST')
283 config.add_view(cls, attr='stop_root',
284 route_name='stop_root')
287def defaults(config, **kwargs):
288 base = globals()
290 AuthView = kwargs.get('AuthView', base['AuthView'])
291 AuthView.defaults(config)
294def includeme(config):
295 defaults(config)