##// END OF EJS Templates
login: make it clear that an invalid came_from is an invalid request
Mads Kiilerich -
r5509:ad131f70 stable
parent child Browse files
Show More
@@ -1,267 +1,270 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.login
15 kallithea.controllers.login
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Login controller for Kallithea
18 Login controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 22, 2010
22 :created_on: Apr 22, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30 import formencode
30 import formencode
31 import urlparse
31 import urlparse
32
32
33 from formencode import htmlfill
33 from formencode import htmlfill
34 from webob.exc import HTTPFound, HTTPBadRequest
34 from webob.exc import HTTPFound, HTTPBadRequest
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
36 from pylons.controllers.util import redirect
37 from pylons import request, session, tmpl_context as c, url
37 from pylons import request, session, tmpl_context as c, url
38
38
39 import kallithea.lib.helpers as h
39 import kallithea.lib.helpers as h
40 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
40 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
41 from kallithea.lib.base import BaseController, log_in_user, render
41 from kallithea.lib.base import BaseController, log_in_user, render
42 from kallithea.lib.exceptions import UserCreationError
42 from kallithea.lib.exceptions import UserCreationError
43 from kallithea.lib.utils2 import safe_str
43 from kallithea.lib.utils2 import safe_str
44 from kallithea.model.db import User, Setting
44 from kallithea.model.db import User, Setting
45 from kallithea.model.forms import \
45 from kallithea.model.forms import \
46 LoginForm, RegisterForm, PasswordResetRequestForm, PasswordResetConfirmationForm
46 LoginForm, RegisterForm, PasswordResetRequestForm, PasswordResetConfirmationForm
47 from kallithea.model.user import UserModel
47 from kallithea.model.user import UserModel
48 from kallithea.model.meta import Session
48 from kallithea.model.meta import Session
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class LoginController(BaseController):
54 class LoginController(BaseController):
55
55
56 def __before__(self):
56 def __before__(self):
57 super(LoginController, self).__before__()
57 super(LoginController, self).__before__()
58
58
59 def _validate_came_from(self, came_from):
59 def _validate_came_from(self, came_from):
60 """Return True if came_from is valid and can and should be used"""
60 """Return True if came_from is valid and can and should be used"""
61 if not came_from:
61 if not came_from:
62 return False
62 return False
63
63
64 parsed = urlparse.urlparse(came_from)
64 parsed = urlparse.urlparse(came_from)
65 server_parsed = urlparse.urlparse(url.current())
65 server_parsed = urlparse.urlparse(url.current())
66 allowed_schemes = ['http', 'https']
66 allowed_schemes = ['http', 'https']
67 if parsed.scheme and parsed.scheme not in allowed_schemes:
67 if parsed.scheme and parsed.scheme not in allowed_schemes:
68 log.error('Suspicious URL scheme detected %s for url %s',
68 log.error('Suspicious URL scheme detected %s for url %s',
69 parsed.scheme, parsed)
69 parsed.scheme, parsed)
70 return False
70 return False
71 if server_parsed.netloc != parsed.netloc:
71 if server_parsed.netloc != parsed.netloc:
72 log.error('Suspicious NETLOC detected %s for url %s server url '
72 log.error('Suspicious NETLOC detected %s for url %s server url '
73 'is: %s' % (parsed.netloc, parsed, server_parsed))
73 'is: %s' % (parsed.netloc, parsed, server_parsed))
74 return False
74 return False
75 return True
75 return True
76
76
77 def index(self):
77 def index(self):
78 c.came_from = safe_str(request.GET.pop('came_from', ''))
78 c.came_from = safe_str(request.GET.pop('came_from', ''))
79 if self._validate_came_from(c.came_from):
79 if c.came_from:
80 if not self._validate_came_from(c.came_from):
81 log.error('Invalid came_from (not server-relative): %r', c.came_from)
82 raise HTTPBadRequest()
80 came_from = url(c.came_from, **request.GET)
83 came_from = url(c.came_from, **request.GET)
81 else:
84 else:
82 c.came_from = came_from = url('home')
85 c.came_from = came_from = url('home')
83
86
84 not_default = self.authuser.username != User.DEFAULT_USER
87 not_default = self.authuser.username != User.DEFAULT_USER
85 ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr)
88 ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr)
86
89
87 # redirect if already logged in
90 # redirect if already logged in
88 if self.authuser.is_authenticated and not_default and ip_allowed:
91 if self.authuser.is_authenticated and not_default and ip_allowed:
89 raise HTTPFound(location=came_from)
92 raise HTTPFound(location=came_from)
90
93
91 if request.POST:
94 if request.POST:
92 # import Login Form validator class
95 # import Login Form validator class
93 login_form = LoginForm()
96 login_form = LoginForm()
94 try:
97 try:
95 c.form_result = login_form.to_python(dict(request.POST))
98 c.form_result = login_form.to_python(dict(request.POST))
96 # form checks for username/password, now we're authenticated
99 # form checks for username/password, now we're authenticated
97 username = c.form_result['username']
100 username = c.form_result['username']
98 user = User.get_by_username(username, case_insensitive=True)
101 user = User.get_by_username(username, case_insensitive=True)
99 except formencode.Invalid as errors:
102 except formencode.Invalid as errors:
100 defaults = errors.value
103 defaults = errors.value
101 # remove password from filling in form again
104 # remove password from filling in form again
102 del defaults['password']
105 del defaults['password']
103 return htmlfill.render(
106 return htmlfill.render(
104 render('/login.html'),
107 render('/login.html'),
105 defaults=errors.value,
108 defaults=errors.value,
106 errors=errors.error_dict or {},
109 errors=errors.error_dict or {},
107 prefix_error=False,
110 prefix_error=False,
108 encoding="UTF-8",
111 encoding="UTF-8",
109 force_defaults=False)
112 force_defaults=False)
110 except UserCreationError as e:
113 except UserCreationError as e:
111 # container auth or other auth functions that create users on
114 # container auth or other auth functions that create users on
112 # the fly can throw this exception signaling that there's issue
115 # the fly can throw this exception signaling that there's issue
113 # with user creation, explanation should be provided in
116 # with user creation, explanation should be provided in
114 # Exception itself
117 # Exception itself
115 h.flash(e, 'error')
118 h.flash(e, 'error')
116 else:
119 else:
117 log_in_user(user, c.form_result['remember'],
120 log_in_user(user, c.form_result['remember'],
118 is_external_auth=False)
121 is_external_auth=False)
119 raise HTTPFound(location=came_from)
122 raise HTTPFound(location=came_from)
120
123
121 return render('/login.html')
124 return render('/login.html')
122
125
123 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
126 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
124 'hg.register.manual_activate')
127 'hg.register.manual_activate')
125 def register(self):
128 def register(self):
126 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
129 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
127 .AuthUser.permissions['global']
130 .AuthUser.permissions['global']
128
131
129 settings = Setting.get_app_settings()
132 settings = Setting.get_app_settings()
130 captcha_private_key = settings.get('captcha_private_key')
133 captcha_private_key = settings.get('captcha_private_key')
131 c.captcha_active = bool(captcha_private_key)
134 c.captcha_active = bool(captcha_private_key)
132 c.captcha_public_key = settings.get('captcha_public_key')
135 c.captcha_public_key = settings.get('captcha_public_key')
133
136
134 if request.POST:
137 if request.POST:
135 register_form = RegisterForm()()
138 register_form = RegisterForm()()
136 try:
139 try:
137 form_result = register_form.to_python(dict(request.POST))
140 form_result = register_form.to_python(dict(request.POST))
138 form_result['active'] = c.auto_active
141 form_result['active'] = c.auto_active
139
142
140 if c.captcha_active:
143 if c.captcha_active:
141 from kallithea.lib.recaptcha import submit
144 from kallithea.lib.recaptcha import submit
142 response = submit(request.POST.get('recaptcha_challenge_field'),
145 response = submit(request.POST.get('recaptcha_challenge_field'),
143 request.POST.get('recaptcha_response_field'),
146 request.POST.get('recaptcha_response_field'),
144 private_key=captcha_private_key,
147 private_key=captcha_private_key,
145 remoteip=self.ip_addr)
148 remoteip=self.ip_addr)
146 if c.captcha_active and not response.is_valid:
149 if c.captcha_active and not response.is_valid:
147 _value = form_result
150 _value = form_result
148 _msg = _('Bad captcha')
151 _msg = _('Bad captcha')
149 error_dict = {'recaptcha_field': _msg}
152 error_dict = {'recaptcha_field': _msg}
150 raise formencode.Invalid(_msg, _value, None,
153 raise formencode.Invalid(_msg, _value, None,
151 error_dict=error_dict)
154 error_dict=error_dict)
152
155
153 UserModel().create_registration(form_result)
156 UserModel().create_registration(form_result)
154 h.flash(_('You have successfully registered into Kallithea'),
157 h.flash(_('You have successfully registered into Kallithea'),
155 category='success')
158 category='success')
156 Session().commit()
159 Session().commit()
157 return redirect(url('login_home'))
160 return redirect(url('login_home'))
158
161
159 except formencode.Invalid as errors:
162 except formencode.Invalid as errors:
160 return htmlfill.render(
163 return htmlfill.render(
161 render('/register.html'),
164 render('/register.html'),
162 defaults=errors.value,
165 defaults=errors.value,
163 errors=errors.error_dict or {},
166 errors=errors.error_dict or {},
164 prefix_error=False,
167 prefix_error=False,
165 encoding="UTF-8",
168 encoding="UTF-8",
166 force_defaults=False)
169 force_defaults=False)
167 except UserCreationError as e:
170 except UserCreationError as e:
168 # container auth or other auth functions that create users on
171 # container auth or other auth functions that create users on
169 # the fly can throw this exception signaling that there's issue
172 # the fly can throw this exception signaling that there's issue
170 # with user creation, explanation should be provided in
173 # with user creation, explanation should be provided in
171 # Exception itself
174 # Exception itself
172 h.flash(e, 'error')
175 h.flash(e, 'error')
173
176
174 return render('/register.html')
177 return render('/register.html')
175
178
176 def password_reset(self):
179 def password_reset(self):
177 settings = Setting.get_app_settings()
180 settings = Setting.get_app_settings()
178 captcha_private_key = settings.get('captcha_private_key')
181 captcha_private_key = settings.get('captcha_private_key')
179 c.captcha_active = bool(captcha_private_key)
182 c.captcha_active = bool(captcha_private_key)
180 c.captcha_public_key = settings.get('captcha_public_key')
183 c.captcha_public_key = settings.get('captcha_public_key')
181
184
182 if request.POST:
185 if request.POST:
183 password_reset_form = PasswordResetRequestForm()()
186 password_reset_form = PasswordResetRequestForm()()
184 try:
187 try:
185 form_result = password_reset_form.to_python(dict(request.POST))
188 form_result = password_reset_form.to_python(dict(request.POST))
186 if c.captcha_active:
189 if c.captcha_active:
187 from kallithea.lib.recaptcha import submit
190 from kallithea.lib.recaptcha import submit
188 response = submit(request.POST.get('recaptcha_challenge_field'),
191 response = submit(request.POST.get('recaptcha_challenge_field'),
189 request.POST.get('recaptcha_response_field'),
192 request.POST.get('recaptcha_response_field'),
190 private_key=captcha_private_key,
193 private_key=captcha_private_key,
191 remoteip=self.ip_addr)
194 remoteip=self.ip_addr)
192 if c.captcha_active and not response.is_valid:
195 if c.captcha_active and not response.is_valid:
193 _value = form_result
196 _value = form_result
194 _msg = _('Bad captcha')
197 _msg = _('Bad captcha')
195 error_dict = {'recaptcha_field': _msg}
198 error_dict = {'recaptcha_field': _msg}
196 raise formencode.Invalid(_msg, _value, None,
199 raise formencode.Invalid(_msg, _value, None,
197 error_dict=error_dict)
200 error_dict=error_dict)
198 redirect_link = UserModel().send_reset_password_email(form_result)
201 redirect_link = UserModel().send_reset_password_email(form_result)
199 h.flash(_('A password reset confirmation code has been sent'),
202 h.flash(_('A password reset confirmation code has been sent'),
200 category='success')
203 category='success')
201 return redirect(redirect_link)
204 return redirect(redirect_link)
202
205
203 except formencode.Invalid as errors:
206 except formencode.Invalid as errors:
204 return htmlfill.render(
207 return htmlfill.render(
205 render('/password_reset.html'),
208 render('/password_reset.html'),
206 defaults=errors.value,
209 defaults=errors.value,
207 errors=errors.error_dict or {},
210 errors=errors.error_dict or {},
208 prefix_error=False,
211 prefix_error=False,
209 encoding="UTF-8",
212 encoding="UTF-8",
210 force_defaults=False)
213 force_defaults=False)
211
214
212 return render('/password_reset.html')
215 return render('/password_reset.html')
213
216
214 def password_reset_confirmation(self):
217 def password_reset_confirmation(self):
215 # This controller handles both GET and POST requests, though we
218 # This controller handles both GET and POST requests, though we
216 # only ever perform the actual password change on POST (since
219 # only ever perform the actual password change on POST (since
217 # GET requests are not allowed to have side effects, and do not
220 # GET requests are not allowed to have side effects, and do not
218 # receive automatic CSRF protection).
221 # receive automatic CSRF protection).
219
222
220 # The template needs the email address outside of the form.
223 # The template needs the email address outside of the form.
221 c.email = request.params.get('email')
224 c.email = request.params.get('email')
222
225
223 if not request.POST:
226 if not request.POST:
224 return htmlfill.render(
227 return htmlfill.render(
225 render('/password_reset_confirmation.html'),
228 render('/password_reset_confirmation.html'),
226 defaults=dict(request.params),
229 defaults=dict(request.params),
227 encoding='UTF-8')
230 encoding='UTF-8')
228
231
229 form = PasswordResetConfirmationForm()()
232 form = PasswordResetConfirmationForm()()
230 try:
233 try:
231 form_result = form.to_python(dict(request.POST))
234 form_result = form.to_python(dict(request.POST))
232 except formencode.Invalid as errors:
235 except formencode.Invalid as errors:
233 return htmlfill.render(
236 return htmlfill.render(
234 render('/password_reset_confirmation.html'),
237 render('/password_reset_confirmation.html'),
235 defaults=errors.value,
238 defaults=errors.value,
236 errors=errors.error_dict or {},
239 errors=errors.error_dict or {},
237 prefix_error=False,
240 prefix_error=False,
238 encoding='UTF-8')
241 encoding='UTF-8')
239
242
240 if not UserModel().verify_reset_password_token(
243 if not UserModel().verify_reset_password_token(
241 form_result['email'],
244 form_result['email'],
242 form_result['timestamp'],
245 form_result['timestamp'],
243 form_result['token'],
246 form_result['token'],
244 ):
247 ):
245 return htmlfill.render(
248 return htmlfill.render(
246 render('/password_reset_confirmation.html'),
249 render('/password_reset_confirmation.html'),
247 defaults=form_result,
250 defaults=form_result,
248 errors={'token': _('Invalid password reset token')},
251 errors={'token': _('Invalid password reset token')},
249 prefix_error=False,
252 prefix_error=False,
250 encoding='UTF-8')
253 encoding='UTF-8')
251
254
252 UserModel().reset_password(form_result['email'], form_result['password'])
255 UserModel().reset_password(form_result['email'], form_result['password'])
253 h.flash(_('Successfully updated password'), category='success')
256 h.flash(_('Successfully updated password'), category='success')
254 return redirect(url('login_home'))
257 return redirect(url('login_home'))
255
258
256 def logout(self):
259 def logout(self):
257 session.delete()
260 session.delete()
258 log.info('Logging out and deleting session for user')
261 log.info('Logging out and deleting session for user')
259 redirect(url('home'))
262 redirect(url('home'))
260
263
261 def authentication_token(self):
264 def authentication_token(self):
262 """Return the CSRF protection token for the session - just like it
265 """Return the CSRF protection token for the session - just like it
263 could have been screen scraped from a page with a form.
266 could have been screen scraped from a page with a form.
264 Only intended for testing but might also be useful for other kinds
267 Only intended for testing but might also be useful for other kinds
265 of automation.
268 of automation.
266 """
269 """
267 return h.authentication_token()
270 return h.authentication_token()
General Comments 0
You need to be logged in to leave comments. Login now