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