##// END OF EJS Templates
auth: make the store_user_in_session public, as it's used in external auth plugins.
marcink -
r3250:7ab39ef0 default
parent child Browse files
Show More
@@ -1,461 +1,461 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 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 import requests
29 29
30 30 from pyramid.httpexceptions import HTTPFound
31 31 from pyramid.view import view_config
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, trigger
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 def _store_user_in_session(session, username, remember=False):
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
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 def validate_captcha(self, private_key):
128 128
129 129 captcha_rs = self.request.POST.get('g-recaptcha-response')
130 130 url = "https://www.google.com/recaptcha/api/siteverify"
131 131 params = {
132 132 'secret': private_key,
133 133 'response': captcha_rs,
134 134 'remoteip': get_ip_addr(self.request.environ)
135 135 }
136 136 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
137 137 verify_rs = verify_rs.json()
138 138 captcha_status = verify_rs.get('success', False)
139 139 captcha_errors = verify_rs.get('error-codes', [])
140 140 if not isinstance(captcha_errors, list):
141 141 captcha_errors = [captcha_errors]
142 142 captcha_errors = ', '.join(captcha_errors)
143 143 captcha_message = ''
144 144 if captcha_status is False:
145 145 captcha_message = "Bad captcha. Errors: {}".format(
146 146 captcha_errors)
147 147
148 148 return captcha_status, captcha_message
149 149
150 150 @view_config(
151 151 route_name='login', request_method='GET',
152 152 renderer='rhodecode:templates/login.mako')
153 153 def login(self):
154 154 c = self.load_default_context()
155 155 auth_user = self._rhodecode_user
156 156
157 157 # redirect if already logged in
158 158 if (auth_user.is_authenticated and
159 159 not auth_user.is_default and auth_user.ip_allowed):
160 160 raise HTTPFound(c.came_from)
161 161
162 162 # check if we use headers plugin, and try to login using it.
163 163 try:
164 164 log.debug('Running PRE-AUTH for headers based authentication')
165 165 auth_info = authenticate(
166 166 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
167 167 if auth_info:
168 headers = _store_user_in_session(
168 headers = store_user_in_session(
169 169 self.session, auth_info.get('username'))
170 170 raise HTTPFound(c.came_from, headers=headers)
171 171 except UserCreationError as e:
172 172 log.error(e)
173 173 h.flash(e, category='error')
174 174
175 175 return self._get_template_context(c)
176 176
177 177 @view_config(
178 178 route_name='login', request_method='POST',
179 179 renderer='rhodecode:templates/login.mako')
180 180 def login_post(self):
181 181 c = self.load_default_context()
182 182
183 183 login_form = LoginForm(self.request.translate)()
184 184
185 185 try:
186 186 self.session.invalidate()
187 187 form_result = login_form.to_python(self.request.POST)
188 188 # form checks for username/password, now we're authenticated
189 headers = _store_user_in_session(
189 headers = store_user_in_session(
190 190 self.session,
191 191 username=form_result['username'],
192 192 remember=form_result['remember'])
193 193 log.debug('Redirecting to "%s" after login.', c.came_from)
194 194
195 195 audit_user = audit_logger.UserWrap(
196 196 username=self.request.POST.get('username'),
197 197 ip_addr=self.request.remote_addr)
198 198 action_data = {'user_agent': self.request.user_agent}
199 199 audit_logger.store_web(
200 200 'user.login.success', action_data=action_data,
201 201 user=audit_user, commit=True)
202 202
203 203 raise HTTPFound(c.came_from, headers=headers)
204 204 except formencode.Invalid as errors:
205 205 defaults = errors.value
206 206 # remove password from filling in form again
207 207 defaults.pop('password', None)
208 208 render_ctx = {
209 209 'errors': errors.error_dict,
210 210 'defaults': defaults,
211 211 }
212 212
213 213 audit_user = audit_logger.UserWrap(
214 214 username=self.request.POST.get('username'),
215 215 ip_addr=self.request.remote_addr)
216 216 action_data = {'user_agent': self.request.user_agent}
217 217 audit_logger.store_web(
218 218 'user.login.failure', action_data=action_data,
219 219 user=audit_user, commit=True)
220 220 return self._get_template_context(c, **render_ctx)
221 221
222 222 except UserCreationError as e:
223 223 # headers auth or other auth functions that create users on
224 224 # the fly can throw this exception signaling that there's issue
225 225 # with user creation, explanation should be provided in
226 226 # Exception itself
227 227 h.flash(e, category='error')
228 228 return self._get_template_context(c)
229 229
230 230 @CSRFRequired()
231 231 @view_config(route_name='logout', request_method='POST')
232 232 def logout(self):
233 233 auth_user = self._rhodecode_user
234 234 log.info('Deleting session for user: `%s`', auth_user)
235 235
236 236 action_data = {'user_agent': self.request.user_agent}
237 237 audit_logger.store_web(
238 238 'user.logout', action_data=action_data,
239 239 user=auth_user, commit=True)
240 240 self.session.delete()
241 241 return HTTPFound(h.route_path('home'))
242 242
243 243 @HasPermissionAnyDecorator(
244 244 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
245 245 @view_config(
246 246 route_name='register', request_method='GET',
247 247 renderer='rhodecode:templates/register.mako',)
248 248 def register(self, defaults=None, errors=None):
249 249 c = self.load_default_context()
250 250 defaults = defaults or {}
251 251 errors = errors or {}
252 252
253 253 settings = SettingsModel().get_all_settings()
254 254 register_message = settings.get('rhodecode_register_message') or ''
255 255 captcha = self._get_captcha_data()
256 256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
257 257 .AuthUser().permissions['global']
258 258
259 259 render_ctx = self._get_template_context(c)
260 260 render_ctx.update({
261 261 'defaults': defaults,
262 262 'errors': errors,
263 263 'auto_active': auto_active,
264 264 'captcha_active': captcha.active,
265 265 'captcha_public_key': captcha.public_key,
266 266 'register_message': register_message,
267 267 })
268 268 return render_ctx
269 269
270 270 @HasPermissionAnyDecorator(
271 271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
272 272 @view_config(
273 273 route_name='register', request_method='POST',
274 274 renderer='rhodecode:templates/register.mako')
275 275 def register_post(self):
276 276 self.load_default_context()
277 277 captcha = self._get_captcha_data()
278 278 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
279 279 .AuthUser().permissions['global']
280 280
281 281 register_form = RegisterForm(self.request.translate)()
282 282 try:
283 283
284 284 form_result = register_form.to_python(self.request.POST)
285 285 form_result['active'] = auto_active
286 286
287 287 if captcha.active:
288 288 captcha_status, captcha_message = self.validate_captcha(
289 289 captcha.private_key)
290 290
291 291 if not captcha_status:
292 292 _value = form_result
293 293 _msg = _('Bad captcha')
294 294 error_dict = {'recaptcha_field': captcha_message}
295 295 raise formencode.Invalid(
296 296 _msg, _value, None, error_dict=error_dict)
297 297
298 298 new_user = UserModel().create_registration(form_result)
299 299
300 300 action_data = {'data': new_user.get_api_data(),
301 301 'user_agent': self.request.user_agent}
302 302
303 303 audit_user = audit_logger.UserWrap(
304 304 username=new_user.username,
305 305 user_id=new_user.user_id,
306 306 ip_addr=self.request.remote_addr)
307 307
308 308 audit_logger.store_web(
309 309 'user.register', action_data=action_data,
310 310 user=audit_user)
311 311
312 312 event = UserRegistered(user=new_user, session=self.session)
313 313 trigger(event)
314 314 h.flash(
315 315 _('You have successfully registered with RhodeCode'),
316 316 category='success')
317 317 Session().commit()
318 318
319 319 redirect_ro = self.request.route_path('login')
320 320 raise HTTPFound(redirect_ro)
321 321
322 322 except formencode.Invalid as errors:
323 323 errors.value.pop('password', None)
324 324 errors.value.pop('password_confirmation', None)
325 325 return self.register(
326 326 defaults=errors.value, errors=errors.error_dict)
327 327
328 328 except UserCreationError as e:
329 329 # container auth or other auth functions that create users on
330 330 # the fly can throw this exception signaling that there's issue
331 331 # with user creation, explanation should be provided in
332 332 # Exception itself
333 333 h.flash(e, category='error')
334 334 return self.register()
335 335
336 336 @view_config(
337 337 route_name='reset_password', request_method=('GET', 'POST'),
338 338 renderer='rhodecode:templates/password_reset.mako')
339 339 def password_reset(self):
340 340 c = self.load_default_context()
341 341 captcha = self._get_captcha_data()
342 342
343 343 template_context = {
344 344 'captcha_active': captcha.active,
345 345 'captcha_public_key': captcha.public_key,
346 346 'defaults': {},
347 347 'errors': {},
348 348 }
349 349
350 350 # always send implicit message to prevent from discovery of
351 351 # matching emails
352 352 msg = _('If such email exists, a password reset link was sent to it.')
353 353
354 354 if self.request.POST:
355 355 if h.HasPermissionAny('hg.password_reset.disabled')():
356 356 _email = self.request.POST.get('email', '')
357 357 log.error('Failed attempt to reset password for `%s`.', _email)
358 358 h.flash(_('Password reset has been disabled.'),
359 359 category='error')
360 360 return HTTPFound(self.request.route_path('reset_password'))
361 361
362 362 password_reset_form = PasswordResetForm(self.request.translate)()
363 363 try:
364 364 form_result = password_reset_form.to_python(
365 365 self.request.POST)
366 366 user_email = form_result['email']
367 367
368 368 if captcha.active:
369 369 captcha_status, captcha_message = self.validate_captcha(
370 370 captcha.private_key)
371 371
372 372 if not captcha_status:
373 373 _value = form_result
374 374 _msg = _('Bad captcha')
375 375 error_dict = {'recaptcha_field': captcha_message}
376 376 raise formencode.Invalid(
377 377 _msg, _value, None, error_dict=error_dict)
378 378
379 379 # Generate reset URL and send mail.
380 380 user = User.get_by_email(user_email)
381 381
382 382 # generate password reset token that expires in 10 minutes
383 383 description = u'Generated token for password reset from {}'.format(
384 384 datetime.datetime.now().isoformat())
385 385
386 386 reset_token = UserModel().add_auth_token(
387 387 user=user, lifetime_minutes=10,
388 388 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
389 389 description=description)
390 390 Session().commit()
391 391
392 392 log.debug('Successfully created password recovery token')
393 393 password_reset_url = self.request.route_url(
394 394 'reset_password_confirmation',
395 395 _query={'key': reset_token.api_key})
396 396 UserModel().reset_password_link(
397 397 form_result, password_reset_url)
398 398 # Display success message and redirect.
399 399 h.flash(msg, category='success')
400 400
401 401 action_data = {'email': user_email,
402 402 'user_agent': self.request.user_agent}
403 403 audit_logger.store_web(
404 404 'user.password.reset_request', action_data=action_data,
405 405 user=self._rhodecode_user, commit=True)
406 406 return HTTPFound(self.request.route_path('reset_password'))
407 407
408 408 except formencode.Invalid as errors:
409 409 template_context.update({
410 410 'defaults': errors.value,
411 411 'errors': errors.error_dict,
412 412 })
413 413 if not self.request.POST.get('email'):
414 414 # case of empty email, we want to report that
415 415 return self._get_template_context(c, **template_context)
416 416
417 417 if 'recaptcha_field' in errors.error_dict:
418 418 # case of failed captcha
419 419 return self._get_template_context(c, **template_context)
420 420
421 421 log.debug('faking response on invalid password reset')
422 422 # make this take 2s, to prevent brute forcing.
423 423 time.sleep(2)
424 424 h.flash(msg, category='success')
425 425 return HTTPFound(self.request.route_path('reset_password'))
426 426
427 427 return self._get_template_context(c, **template_context)
428 428
429 429 @view_config(route_name='reset_password_confirmation',
430 430 request_method='GET')
431 431 def password_reset_confirmation(self):
432 432 self.load_default_context()
433 433 if self.request.GET and self.request.GET.get('key'):
434 434 # make this take 2s, to prevent brute forcing.
435 435 time.sleep(2)
436 436
437 437 token = AuthTokenModel().get_auth_token(
438 438 self.request.GET.get('key'))
439 439
440 440 # verify token is the correct role
441 441 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
442 442 log.debug('Got token with role:%s expected is %s',
443 443 getattr(token, 'role', 'EMPTY_TOKEN'),
444 444 UserApiKeys.ROLE_PASSWORD_RESET)
445 445 h.flash(
446 446 _('Given reset token is invalid'), category='error')
447 447 return HTTPFound(self.request.route_path('reset_password'))
448 448
449 449 try:
450 450 owner = token.user
451 451 data = {'email': owner.email, 'token': token.api_key}
452 452 UserModel().reset_password(data)
453 453 h.flash(
454 454 _('Your password reset was successful, '
455 455 'a new password has been sent to your email'),
456 456 category='success')
457 457 except Exception as e:
458 458 log.error(e)
459 459 return HTTPFound(self.request.route_path('reset_password'))
460 460
461 461 return HTTPFound(self.request.route_path('login'))
General Comments 0
You need to be logged in to leave comments. Login now