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