##// END OF EJS Templates
audit: properly store IP address for user registration
marcink -
r2732:6debc24c default
parent child Browse files
Show More
@@ -1,454 +1,460 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 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)
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 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 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
303 audit_user = audit_logger.UserWrap(
304 username=new_user.username,
305 user_id=new_user.user_id,
306 ip_addr=self.request.remote_addr)
307
302 308 audit_logger.store_web(
303 309 'user.register', action_data=action_data,
304 user=new_user)
310 user=audit_user)
305 311
306 312 event = UserRegistered(user=new_user, session=self.session)
307 313 trigger(event)
308 314 h.flash(
309 315 _('You have successfully registered with RhodeCode'),
310 316 category='success')
311 317 Session().commit()
312 318
313 319 redirect_ro = self.request.route_path('login')
314 320 raise HTTPFound(redirect_ro)
315 321
316 322 except formencode.Invalid as errors:
317 323 errors.value.pop('password', None)
318 324 errors.value.pop('password_confirmation', None)
319 325 return self.register(
320 326 defaults=errors.value, errors=errors.error_dict)
321 327
322 328 except UserCreationError as e:
323 329 # container auth or other auth functions that create users on
324 330 # the fly can throw this exception signaling that there's issue
325 331 # with user creation, explanation should be provided in
326 332 # Exception itself
327 333 h.flash(e, category='error')
328 334 return self.register()
329 335
330 336 @view_config(
331 337 route_name='reset_password', request_method=('GET', 'POST'),
332 338 renderer='rhodecode:templates/password_reset.mako')
333 339 def password_reset(self):
334 340 c = self.load_default_context()
335 341 captcha = self._get_captcha_data()
336 342
337 343 template_context = {
338 344 'captcha_active': captcha.active,
339 345 'captcha_public_key': captcha.public_key,
340 346 'defaults': {},
341 347 'errors': {},
342 348 }
343 349
344 350 # always send implicit message to prevent from discovery of
345 351 # matching emails
346 352 msg = _('If such email exists, a password reset link was sent to it.')
347 353
348 354 if self.request.POST:
349 355 if h.HasPermissionAny('hg.password_reset.disabled')():
350 356 _email = self.request.POST.get('email', '')
351 357 log.error('Failed attempt to reset password for `%s`.', _email)
352 358 h.flash(_('Password reset has been disabled.'),
353 359 category='error')
354 360 return HTTPFound(self.request.route_path('reset_password'))
355 361
356 362 password_reset_form = PasswordResetForm(self.request.translate)()
357 363 try:
358 364 form_result = password_reset_form.to_python(
359 365 self.request.POST)
360 366 user_email = form_result['email']
361 367
362 368 if captcha.active:
363 369 captcha_status, captcha_message = self.validate_captcha(
364 370 captcha.private_key)
365 371
366 372 if not captcha_status:
367 373 _value = form_result
368 374 _msg = _('Bad captcha')
369 375 error_dict = {'recaptcha_field': captcha_message}
370 376 raise formencode.Invalid(
371 377 _msg, _value, None, error_dict=error_dict)
372 378
373 379 # Generate reset URL and send mail.
374 380 user = User.get_by_email(user_email)
375 381
376 382 # generate password reset token that expires in 10minutes
377 383 desc = 'Generated token for password reset from {}'.format(
378 384 datetime.datetime.now().isoformat())
379 385 reset_token = AuthTokenModel().create(
380 386 user, lifetime=10,
381 387 description=desc,
382 388 role=UserApiKeys.ROLE_PASSWORD_RESET)
383 389 Session().commit()
384 390
385 391 log.debug('Successfully created password recovery token')
386 392 password_reset_url = self.request.route_url(
387 393 'reset_password_confirmation',
388 394 _query={'key': reset_token.api_key})
389 395 UserModel().reset_password_link(
390 396 form_result, password_reset_url)
391 397 # Display success message and redirect.
392 398 h.flash(msg, category='success')
393 399
394 400 action_data = {'email': user_email,
395 401 'user_agent': self.request.user_agent}
396 402 audit_logger.store_web(
397 403 'user.password.reset_request', action_data=action_data,
398 404 user=self._rhodecode_user, commit=True)
399 405 return HTTPFound(self.request.route_path('reset_password'))
400 406
401 407 except formencode.Invalid as errors:
402 408 template_context.update({
403 409 'defaults': errors.value,
404 410 'errors': errors.error_dict,
405 411 })
406 412 if not self.request.POST.get('email'):
407 413 # case of empty email, we want to report that
408 414 return self._get_template_context(c, **template_context)
409 415
410 416 if 'recaptcha_field' in errors.error_dict:
411 417 # case of failed captcha
412 418 return self._get_template_context(c, **template_context)
413 419
414 420 log.debug('faking response on invalid password reset')
415 421 # make this take 2s, to prevent brute forcing.
416 422 time.sleep(2)
417 423 h.flash(msg, category='success')
418 424 return HTTPFound(self.request.route_path('reset_password'))
419 425
420 426 return self._get_template_context(c, **template_context)
421 427
422 428 @view_config(route_name='reset_password_confirmation',
423 429 request_method='GET')
424 430 def password_reset_confirmation(self):
425 431 self.load_default_context()
426 432 if self.request.GET and self.request.GET.get('key'):
427 433 # make this take 2s, to prevent brute forcing.
428 434 time.sleep(2)
429 435
430 436 token = AuthTokenModel().get_auth_token(
431 437 self.request.GET.get('key'))
432 438
433 439 # verify token is the correct role
434 440 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
435 441 log.debug('Got token with role:%s expected is %s',
436 442 getattr(token, 'role', 'EMPTY_TOKEN'),
437 443 UserApiKeys.ROLE_PASSWORD_RESET)
438 444 h.flash(
439 445 _('Given reset token is invalid'), category='error')
440 446 return HTTPFound(self.request.route_path('reset_password'))
441 447
442 448 try:
443 449 owner = token.user
444 450 data = {'email': owner.email, 'token': token.api_key}
445 451 UserModel().reset_password(data)
446 452 h.flash(
447 453 _('Your password reset was successful, '
448 454 'a new password has been sent to your email'),
449 455 category='success')
450 456 except Exception as e:
451 457 log.error(e)
452 458 return HTTPFound(self.request.route_path('reset_password'))
453 459
454 460 return HTTPFound(self.request.route_path('login'))
General Comments 0
You need to be logged in to leave comments. Login now