##// END OF EJS Templates
login: don't show password hash inside the logs. It's irrelevant to show this.
marcink -
r1291:b5aef8f8 default
parent child Browse files
Show More
@@ -1,355 +1,357 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import collections
21 import collections
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import logging
24 import logging
25 import urlparse
25 import urlparse
26
26
27 from pylons import url
27 from pylons import url
28 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from recaptcha.client.captcha import submit
30 from recaptcha.client.captcha import submit
31
31
32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.events import UserRegistered
33 from rhodecode.events import UserRegistered
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
36 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
37 from rhodecode.lib.base import get_ip_addr
37 from rhodecode.lib.base import get_ip_addr
38 from rhodecode.lib.exceptions import UserCreationError
38 from rhodecode.lib.exceptions import UserCreationError
39 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
41 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
42 from rhodecode.model.login_session import LoginSession
42 from rhodecode.model.login_session import LoginSession
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.translation import _
46 from rhodecode.translation import _
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51 CaptchaData = collections.namedtuple(
51 CaptchaData = collections.namedtuple(
52 'CaptchaData', 'active, private_key, public_key')
52 'CaptchaData', 'active, private_key, public_key')
53
53
54
54
55 def _store_user_in_session(session, username, remember=False):
55 def _store_user_in_session(session, username, remember=False):
56 user = User.get_by_username(username, case_insensitive=True)
56 user = User.get_by_username(username, case_insensitive=True)
57 auth_user = AuthUser(user.user_id)
57 auth_user = AuthUser(user.user_id)
58 auth_user.set_authenticated()
58 auth_user.set_authenticated()
59 cs = auth_user.get_cookie_store()
59 cs = auth_user.get_cookie_store()
60 session['rhodecode_user'] = cs
60 session['rhodecode_user'] = cs
61 user.update_lastlogin()
61 user.update_lastlogin()
62 Session().commit()
62 Session().commit()
63
63
64 # If they want to be remembered, update the cookie
64 # If they want to be remembered, update the cookie
65 if remember:
65 if remember:
66 _year = (datetime.datetime.now() +
66 _year = (datetime.datetime.now() +
67 datetime.timedelta(seconds=60 * 60 * 24 * 365))
67 datetime.timedelta(seconds=60 * 60 * 24 * 365))
68 session._set_cookie_expires(_year)
68 session._set_cookie_expires(_year)
69
69
70 session.save()
70 session.save()
71
71
72 safe_cs = cs.copy()
73 safe_cs['password'] = '****'
72 log.info('user %s is now authenticated and stored in '
74 log.info('user %s is now authenticated and stored in '
73 'session, session attrs %s', username, cs)
75 'session, session attrs %s', username, safe_cs)
74
76
75 # dumps session attrs back to cookie
77 # dumps session attrs back to cookie
76 session._update_cookie_out()
78 session._update_cookie_out()
77 # we set new cookie
79 # we set new cookie
78 headers = None
80 headers = None
79 if session.request['set_cookie']:
81 if session.request['set_cookie']:
80 # send set-cookie headers back to response to update cookie
82 # send set-cookie headers back to response to update cookie
81 headers = [('Set-Cookie', session.request['cookie_out'])]
83 headers = [('Set-Cookie', session.request['cookie_out'])]
82 return headers
84 return headers
83
85
84
86
85 def get_came_from(request):
87 def get_came_from(request):
86 came_from = safe_str(request.GET.get('came_from', ''))
88 came_from = safe_str(request.GET.get('came_from', ''))
87 parsed = urlparse.urlparse(came_from)
89 parsed = urlparse.urlparse(came_from)
88 allowed_schemes = ['http', 'https']
90 allowed_schemes = ['http', 'https']
89 if parsed.scheme and parsed.scheme not in allowed_schemes:
91 if parsed.scheme and parsed.scheme not in allowed_schemes:
90 log.error('Suspicious URL scheme detected %s for url %s' %
92 log.error('Suspicious URL scheme detected %s for url %s' %
91 (parsed.scheme, parsed))
93 (parsed.scheme, parsed))
92 came_from = url('home')
94 came_from = url('home')
93 elif parsed.netloc and request.host != parsed.netloc:
95 elif parsed.netloc and request.host != parsed.netloc:
94 log.error('Suspicious NETLOC detected %s for url %s server url '
96 log.error('Suspicious NETLOC detected %s for url %s server url '
95 'is: %s' % (parsed.netloc, parsed, request.host))
97 'is: %s' % (parsed.netloc, parsed, request.host))
96 came_from = url('home')
98 came_from = url('home')
97 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
99 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
98 log.error('Header injection detected `%s` for url %s server url ' %
100 log.error('Header injection detected `%s` for url %s server url ' %
99 (parsed.path, parsed))
101 (parsed.path, parsed))
100 came_from = url('home')
102 came_from = url('home')
101
103
102 return came_from or url('home')
104 return came_from or url('home')
103
105
104
106
105 class LoginView(object):
107 class LoginView(object):
106
108
107 def __init__(self, context, request):
109 def __init__(self, context, request):
108 self.request = request
110 self.request = request
109 self.context = context
111 self.context = context
110 self.session = request.session
112 self.session = request.session
111 self._rhodecode_user = request.user
113 self._rhodecode_user = request.user
112
114
113 def _get_template_context(self):
115 def _get_template_context(self):
114 return {
116 return {
115 'came_from': get_came_from(self.request),
117 'came_from': get_came_from(self.request),
116 'defaults': {},
118 'defaults': {},
117 'errors': {},
119 'errors': {},
118 }
120 }
119
121
120 def _get_captcha_data(self):
122 def _get_captcha_data(self):
121 settings = SettingsModel().get_all_settings()
123 settings = SettingsModel().get_all_settings()
122 private_key = settings.get('rhodecode_captcha_private_key')
124 private_key = settings.get('rhodecode_captcha_private_key')
123 public_key = settings.get('rhodecode_captcha_public_key')
125 public_key = settings.get('rhodecode_captcha_public_key')
124 active = bool(private_key)
126 active = bool(private_key)
125 return CaptchaData(
127 return CaptchaData(
126 active=active, private_key=private_key, public_key=public_key)
128 active=active, private_key=private_key, public_key=public_key)
127
129
128 @view_config(
130 @view_config(
129 route_name='login', request_method='GET',
131 route_name='login', request_method='GET',
130 renderer='rhodecode:templates/login.mako')
132 renderer='rhodecode:templates/login.mako')
131 def login(self):
133 def login(self):
132 came_from = get_came_from(self.request)
134 came_from = get_came_from(self.request)
133 user = self.request.user
135 user = self.request.user
134
136
135 # redirect if already logged in
137 # redirect if already logged in
136 if user.is_authenticated and not user.is_default and user.ip_allowed:
138 if user.is_authenticated and not user.is_default and user.ip_allowed:
137 raise HTTPFound(came_from)
139 raise HTTPFound(came_from)
138
140
139 # check if we use headers plugin, and try to login using it.
141 # check if we use headers plugin, and try to login using it.
140 try:
142 try:
141 log.debug('Running PRE-AUTH for headers based authentication')
143 log.debug('Running PRE-AUTH for headers based authentication')
142 auth_info = authenticate(
144 auth_info = authenticate(
143 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
145 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
144 if auth_info:
146 if auth_info:
145 headers = _store_user_in_session(
147 headers = _store_user_in_session(
146 self.session, auth_info.get('username'))
148 self.session, auth_info.get('username'))
147 raise HTTPFound(came_from, headers=headers)
149 raise HTTPFound(came_from, headers=headers)
148 except UserCreationError as e:
150 except UserCreationError as e:
149 log.error(e)
151 log.error(e)
150 self.session.flash(e, queue='error')
152 self.session.flash(e, queue='error')
151
153
152 return self._get_template_context()
154 return self._get_template_context()
153
155
154 @view_config(
156 @view_config(
155 route_name='login', request_method='POST',
157 route_name='login', request_method='POST',
156 renderer='rhodecode:templates/login.mako')
158 renderer='rhodecode:templates/login.mako')
157 def login_post(self):
159 def login_post(self):
158 came_from = get_came_from(self.request)
160 came_from = get_came_from(self.request)
159 session = self.request.session
161 session = self.request.session
160 login_form = LoginForm()()
162 login_form = LoginForm()()
161
163
162 try:
164 try:
163 session.invalidate()
165 session.invalidate()
164 form_result = login_form.to_python(self.request.params)
166 form_result = login_form.to_python(self.request.params)
165 # form checks for username/password, now we're authenticated
167 # form checks for username/password, now we're authenticated
166 headers = _store_user_in_session(
168 headers = _store_user_in_session(
167 self.session,
169 self.session,
168 username=form_result['username'],
170 username=form_result['username'],
169 remember=form_result['remember'])
171 remember=form_result['remember'])
170 log.debug('Redirecting to "%s" after login.', came_from)
172 log.debug('Redirecting to "%s" after login.', came_from)
171 raise HTTPFound(came_from, headers=headers)
173 raise HTTPFound(came_from, headers=headers)
172 except formencode.Invalid as errors:
174 except formencode.Invalid as errors:
173 defaults = errors.value
175 defaults = errors.value
174 # remove password from filling in form again
176 # remove password from filling in form again
175 defaults.pop('password', None)
177 defaults.pop('password', None)
176 render_ctx = self._get_template_context()
178 render_ctx = self._get_template_context()
177 render_ctx.update({
179 render_ctx.update({
178 'errors': errors.error_dict,
180 'errors': errors.error_dict,
179 'defaults': defaults,
181 'defaults': defaults,
180 })
182 })
181 return render_ctx
183 return render_ctx
182
184
183 except UserCreationError as e:
185 except UserCreationError as e:
184 # headers auth or other auth functions that create users on
186 # headers auth or other auth functions that create users on
185 # the fly can throw this exception signaling that there's issue
187 # the fly can throw this exception signaling that there's issue
186 # with user creation, explanation should be provided in
188 # with user creation, explanation should be provided in
187 # Exception itself
189 # Exception itself
188 session.flash(e, queue='error')
190 session.flash(e, queue='error')
189 return self._get_template_context()
191 return self._get_template_context()
190
192
191 @CSRFRequired()
193 @CSRFRequired()
192 @view_config(route_name='logout', request_method='POST')
194 @view_config(route_name='logout', request_method='POST')
193 def logout(self):
195 def logout(self):
194 LoginSession().destroy_user_session()
196 LoginSession().destroy_user_session()
195 return HTTPFound(url('home'))
197 return HTTPFound(url('home'))
196
198
197 @HasPermissionAnyDecorator(
199 @HasPermissionAnyDecorator(
198 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
200 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
199 @view_config(
201 @view_config(
200 route_name='register', request_method='GET',
202 route_name='register', request_method='GET',
201 renderer='rhodecode:templates/register.mako',)
203 renderer='rhodecode:templates/register.mako',)
202 def register(self, defaults=None, errors=None):
204 def register(self, defaults=None, errors=None):
203 defaults = defaults or {}
205 defaults = defaults or {}
204 errors = errors or {}
206 errors = errors or {}
205
207
206 settings = SettingsModel().get_all_settings()
208 settings = SettingsModel().get_all_settings()
207 register_message = settings.get('rhodecode_register_message') or ''
209 register_message = settings.get('rhodecode_register_message') or ''
208 captcha = self._get_captcha_data()
210 captcha = self._get_captcha_data()
209 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
211 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
210 .AuthUser.permissions['global']
212 .AuthUser.permissions['global']
211
213
212 render_ctx = self._get_template_context()
214 render_ctx = self._get_template_context()
213 render_ctx.update({
215 render_ctx.update({
214 'defaults': defaults,
216 'defaults': defaults,
215 'errors': errors,
217 'errors': errors,
216 'auto_active': auto_active,
218 'auto_active': auto_active,
217 'captcha_active': captcha.active,
219 'captcha_active': captcha.active,
218 'captcha_public_key': captcha.public_key,
220 'captcha_public_key': captcha.public_key,
219 'register_message': register_message,
221 'register_message': register_message,
220 })
222 })
221 return render_ctx
223 return render_ctx
222
224
223 @HasPermissionAnyDecorator(
225 @HasPermissionAnyDecorator(
224 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
226 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
225 @view_config(
227 @view_config(
226 route_name='register', request_method='POST',
228 route_name='register', request_method='POST',
227 renderer='rhodecode:templates/register.mako')
229 renderer='rhodecode:templates/register.mako')
228 def register_post(self):
230 def register_post(self):
229 captcha = self._get_captcha_data()
231 captcha = self._get_captcha_data()
230 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
232 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
231 .AuthUser.permissions['global']
233 .AuthUser.permissions['global']
232
234
233 register_form = RegisterForm()()
235 register_form = RegisterForm()()
234 try:
236 try:
235 form_result = register_form.to_python(self.request.params)
237 form_result = register_form.to_python(self.request.params)
236 form_result['active'] = auto_active
238 form_result['active'] = auto_active
237
239
238 if captcha.active:
240 if captcha.active:
239 response = submit(
241 response = submit(
240 self.request.params.get('recaptcha_challenge_field'),
242 self.request.params.get('recaptcha_challenge_field'),
241 self.request.params.get('recaptcha_response_field'),
243 self.request.params.get('recaptcha_response_field'),
242 private_key=captcha.private_key,
244 private_key=captcha.private_key,
243 remoteip=get_ip_addr(self.request.environ))
245 remoteip=get_ip_addr(self.request.environ))
244 if not response.is_valid:
246 if not response.is_valid:
245 _value = form_result
247 _value = form_result
246 _msg = _('Bad captcha')
248 _msg = _('Bad captcha')
247 error_dict = {'recaptcha_field': _msg}
249 error_dict = {'recaptcha_field': _msg}
248 raise formencode.Invalid(_msg, _value, None,
250 raise formencode.Invalid(_msg, _value, None,
249 error_dict=error_dict)
251 error_dict=error_dict)
250
252
251 new_user = UserModel().create_registration(form_result)
253 new_user = UserModel().create_registration(form_result)
252 event = UserRegistered(user=new_user, session=self.session)
254 event = UserRegistered(user=new_user, session=self.session)
253 self.request.registry.notify(event)
255 self.request.registry.notify(event)
254 self.session.flash(
256 self.session.flash(
255 _('You have successfully registered with RhodeCode'),
257 _('You have successfully registered with RhodeCode'),
256 queue='success')
258 queue='success')
257 Session().commit()
259 Session().commit()
258
260
259 redirect_ro = self.request.route_path('login')
261 redirect_ro = self.request.route_path('login')
260 raise HTTPFound(redirect_ro)
262 raise HTTPFound(redirect_ro)
261
263
262 except formencode.Invalid as errors:
264 except formencode.Invalid as errors:
263 errors.value.pop('password', None)
265 errors.value.pop('password', None)
264 errors.value.pop('password_confirmation', None)
266 errors.value.pop('password_confirmation', None)
265 return self.register(
267 return self.register(
266 defaults=errors.value, errors=errors.error_dict)
268 defaults=errors.value, errors=errors.error_dict)
267
269
268 except UserCreationError as e:
270 except UserCreationError as e:
269 # container auth or other auth functions that create users on
271 # container auth or other auth functions that create users on
270 # the fly can throw this exception signaling that there's issue
272 # the fly can throw this exception signaling that there's issue
271 # with user creation, explanation should be provided in
273 # with user creation, explanation should be provided in
272 # Exception itself
274 # Exception itself
273 self.session.flash(e, queue='error')
275 self.session.flash(e, queue='error')
274 return self.register()
276 return self.register()
275
277
276 @view_config(
278 @view_config(
277 route_name='reset_password', request_method=('GET', 'POST'),
279 route_name='reset_password', request_method=('GET', 'POST'),
278 renderer='rhodecode:templates/password_reset.mako')
280 renderer='rhodecode:templates/password_reset.mako')
279 def password_reset(self):
281 def password_reset(self):
280 captcha = self._get_captcha_data()
282 captcha = self._get_captcha_data()
281
283
282 render_ctx = {
284 render_ctx = {
283 'captcha_active': captcha.active,
285 'captcha_active': captcha.active,
284 'captcha_public_key': captcha.public_key,
286 'captcha_public_key': captcha.public_key,
285 'defaults': {},
287 'defaults': {},
286 'errors': {},
288 'errors': {},
287 }
289 }
288
290
289 if self.request.POST:
291 if self.request.POST:
290 password_reset_form = PasswordResetForm()()
292 password_reset_form = PasswordResetForm()()
291 try:
293 try:
292 form_result = password_reset_form.to_python(
294 form_result = password_reset_form.to_python(
293 self.request.params)
295 self.request.params)
294 if h.HasPermissionAny('hg.password_reset.disabled')():
296 if h.HasPermissionAny('hg.password_reset.disabled')():
295 log.error('Failed attempt to reset password for %s.', form_result['email'] )
297 log.error('Failed attempt to reset password for %s.', form_result['email'] )
296 self.session.flash(
298 self.session.flash(
297 _('Password reset has been disabled.'),
299 _('Password reset has been disabled.'),
298 queue='error')
300 queue='error')
299 return HTTPFound(self.request.route_path('reset_password'))
301 return HTTPFound(self.request.route_path('reset_password'))
300 if captcha.active:
302 if captcha.active:
301 response = submit(
303 response = submit(
302 self.request.params.get('recaptcha_challenge_field'),
304 self.request.params.get('recaptcha_challenge_field'),
303 self.request.params.get('recaptcha_response_field'),
305 self.request.params.get('recaptcha_response_field'),
304 private_key=captcha.private_key,
306 private_key=captcha.private_key,
305 remoteip=get_ip_addr(self.request.environ))
307 remoteip=get_ip_addr(self.request.environ))
306 if not response.is_valid:
308 if not response.is_valid:
307 _value = form_result
309 _value = form_result
308 _msg = _('Bad captcha')
310 _msg = _('Bad captcha')
309 error_dict = {'recaptcha_field': _msg}
311 error_dict = {'recaptcha_field': _msg}
310 raise formencode.Invalid(_msg, _value, None,
312 raise formencode.Invalid(_msg, _value, None,
311 error_dict=error_dict)
313 error_dict=error_dict)
312
314
313 # Generate reset URL and send mail.
315 # Generate reset URL and send mail.
314 user_email = form_result['email']
316 user_email = form_result['email']
315 user = User.get_by_email(user_email)
317 user = User.get_by_email(user_email)
316 password_reset_url = self.request.route_url(
318 password_reset_url = self.request.route_url(
317 'reset_password_confirmation',
319 'reset_password_confirmation',
318 _query={'key': user.api_key})
320 _query={'key': user.api_key})
319 UserModel().reset_password_link(
321 UserModel().reset_password_link(
320 form_result, password_reset_url)
322 form_result, password_reset_url)
321
323
322 # Display success message and redirect.
324 # Display success message and redirect.
323 self.session.flash(
325 self.session.flash(
324 _('Your password reset link was sent'),
326 _('Your password reset link was sent'),
325 queue='success')
327 queue='success')
326 return HTTPFound(self.request.route_path('login'))
328 return HTTPFound(self.request.route_path('login'))
327
329
328 except formencode.Invalid as errors:
330 except formencode.Invalid as errors:
329 render_ctx.update({
331 render_ctx.update({
330 'defaults': errors.value,
332 'defaults': errors.value,
331 'errors': errors.error_dict,
333 'errors': errors.error_dict,
332 })
334 })
333
335
334 return render_ctx
336 return render_ctx
335
337
336 @view_config(route_name='reset_password_confirmation',
338 @view_config(route_name='reset_password_confirmation',
337 request_method='GET')
339 request_method='GET')
338 def password_reset_confirmation(self):
340 def password_reset_confirmation(self):
339 if self.request.GET and self.request.GET.get('key'):
341 if self.request.GET and self.request.GET.get('key'):
340 try:
342 try:
341 user = User.get_by_auth_token(self.request.GET.get('key'))
343 user = User.get_by_auth_token(self.request.GET.get('key'))
342 password_reset_url = self.request.route_url(
344 password_reset_url = self.request.route_url(
343 'reset_password_confirmation',
345 'reset_password_confirmation',
344 _query={'key': user.api_key})
346 _query={'key': user.api_key})
345 data = {'email': user.email}
347 data = {'email': user.email}
346 UserModel().reset_password(data, password_reset_url)
348 UserModel().reset_password(data, password_reset_url)
347 self.session.flash(
349 self.session.flash(
348 _('Your password reset was successful, '
350 _('Your password reset was successful, '
349 'a new password has been sent to your email'),
351 'a new password has been sent to your email'),
350 queue='success')
352 queue='success')
351 except Exception as e:
353 except Exception as e:
352 log.error(e)
354 log.error(e)
353 return HTTPFound(self.request.route_path('reset_password'))
355 return HTTPFound(self.request.route_path('reset_password'))
354
356
355 return HTTPFound(self.request.route_path('login'))
357 return HTTPFound(self.request.route_path('login'))
General Comments 0
You need to be logged in to leave comments. Login now