##// END OF EJS Templates
audit-logs: store user agent for login/logout actions.
marcink -
r1702:d464b1a6 default
parent child Browse files
Show More
@@ -1,416 +1,425 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 logging
26 26 import urlparse
27 27
28 28 from pylons import url
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 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 95 log.error('Suspicious URL scheme detected %s for url %s' %
96 96 (parsed.scheme, parsed))
97 97 came_from = url('home')
98 98 elif parsed.netloc and request.host != parsed.netloc:
99 99 log.error('Suspicious NETLOC detected %s for url %s server url '
100 100 'is: %s' % (parsed.netloc, parsed, request.host))
101 101 came_from = url('home')
102 102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 103 log.error('Header injection detected `%s` for url %s server url ' %
104 104 (parsed.path, parsed))
105 105 came_from = url('home')
106 106
107 107 return came_from or url('home')
108 108
109 109
110 110 class LoginView(BaseAppView):
111 111
112 112 def load_default_context(self):
113 113 c = self._get_local_tmpl_context()
114 114 c.came_from = get_came_from(self.request)
115 115 self._register_global_c(c)
116 116 return c
117 117
118 118 def _get_captcha_data(self):
119 119 settings = SettingsModel().get_all_settings()
120 120 private_key = settings.get('rhodecode_captcha_private_key')
121 121 public_key = settings.get('rhodecode_captcha_public_key')
122 122 active = bool(private_key)
123 123 return CaptchaData(
124 124 active=active, private_key=private_key, public_key=public_key)
125 125
126 126 @view_config(
127 127 route_name='login', request_method='GET',
128 128 renderer='rhodecode:templates/login.mako')
129 129 def login(self):
130 130 c = self.load_default_context()
131 131 auth_user = self._rhodecode_user
132 132
133 133 # redirect if already logged in
134 134 if (auth_user.is_authenticated and
135 135 not auth_user.is_default and auth_user.ip_allowed):
136 136 raise HTTPFound(c.came_from)
137 137
138 138 # check if we use headers plugin, and try to login using it.
139 139 try:
140 140 log.debug('Running PRE-AUTH for headers based authentication')
141 141 auth_info = authenticate(
142 142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 143 if auth_info:
144 144 headers = _store_user_in_session(
145 145 self.session, auth_info.get('username'))
146 146 raise HTTPFound(c.came_from, headers=headers)
147 147 except UserCreationError as e:
148 148 log.error(e)
149 149 self.session.flash(e, queue='error')
150 150
151 151 return self._get_template_context(c)
152 152
153 153 @view_config(
154 154 route_name='login', request_method='POST',
155 155 renderer='rhodecode:templates/login.mako')
156 156 def login_post(self):
157 157 c = self.load_default_context()
158 158
159 159 login_form = LoginForm()()
160 160
161 161 try:
162 162 self.session.invalidate()
163 163 form_result = login_form.to_python(self.request.params)
164 164 # form checks for username/password, now we're authenticated
165 165 headers = _store_user_in_session(
166 166 self.session,
167 167 username=form_result['username'],
168 168 remember=form_result['remember'])
169 169 log.debug('Redirecting to "%s" after login.', c.came_from)
170 170
171 171 audit_user = audit_logger.UserWrap(
172 172 username=self.request.params.get('username'),
173 173 ip_addr=self.request.remote_addr)
174 audit_logger.store(action='user.login.success', user=audit_user,
175 commit=True)
174 action_data = {'user_agent': self.request.user_agent}
175 audit_logger.store(
176 action='user.login.success', action_data=action_data,
177 user=audit_user, commit=True)
176 178
177 179 raise HTTPFound(c.came_from, headers=headers)
178 180 except formencode.Invalid as errors:
179 181 defaults = errors.value
180 182 # remove password from filling in form again
181 183 defaults.pop('password', None)
182 184 render_ctx = self._get_template_context(c)
183 185 render_ctx.update({
184 186 'errors': errors.error_dict,
185 187 'defaults': defaults,
186 188 })
187 189
188 190 audit_user = audit_logger.UserWrap(
189 191 username=self.request.params.get('username'),
190 192 ip_addr=self.request.remote_addr)
191 audit_logger.store(action='user.login.failure', user=audit_user,
192 commit=True)
193 action_data = {'user_agent': self.request.user_agent}
194 audit_logger.store(
195 action='user.login.failure', action_data=action_data,
196 user=audit_user, commit=True)
193 197 return render_ctx
194 198
195 199 except UserCreationError as e:
196 200 # headers auth or other auth functions that create users on
197 201 # the fly can throw this exception signaling that there's issue
198 202 # with user creation, explanation should be provided in
199 203 # Exception itself
200 204 self.session.flash(e, queue='error')
201 205 return self._get_template_context(c)
202 206
203 207 @CSRFRequired()
204 208 @view_config(route_name='logout', request_method='POST')
205 209 def logout(self):
206 210 auth_user = self._rhodecode_user
207 211 log.info('Deleting session for user: `%s`', auth_user)
208 audit_logger.store(action='user.logout', user=auth_user,
209 commit=True)
212
213 action_data = {'user_agent': self.request.user_agent}
214 audit_logger.store(
215 action='user.logout', action_data=action_data,
216 user=auth_user, commit=True)
210 217 self.session.delete()
211 218 return HTTPFound(url('home'))
212 219
213 220 @HasPermissionAnyDecorator(
214 221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
215 222 @view_config(
216 223 route_name='register', request_method='GET',
217 224 renderer='rhodecode:templates/register.mako',)
218 225 def register(self, defaults=None, errors=None):
219 226 c = self.load_default_context()
220 227 defaults = defaults or {}
221 228 errors = errors or {}
222 229
223 230 settings = SettingsModel().get_all_settings()
224 231 register_message = settings.get('rhodecode_register_message') or ''
225 232 captcha = self._get_captcha_data()
226 233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
227 234 .AuthUser.permissions['global']
228 235
229 236 render_ctx = self._get_template_context(c)
230 237 render_ctx.update({
231 238 'defaults': defaults,
232 239 'errors': errors,
233 240 'auto_active': auto_active,
234 241 'captcha_active': captcha.active,
235 242 'captcha_public_key': captcha.public_key,
236 243 'register_message': register_message,
237 244 })
238 245 return render_ctx
239 246
240 247 @HasPermissionAnyDecorator(
241 248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
242 249 @view_config(
243 250 route_name='register', request_method='POST',
244 251 renderer='rhodecode:templates/register.mako')
245 252 def register_post(self):
246 253 captcha = self._get_captcha_data()
247 254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
248 255 .AuthUser.permissions['global']
249 256
250 257 register_form = RegisterForm()()
251 258 try:
252 259 form_result = register_form.to_python(self.request.params)
253 260 form_result['active'] = auto_active
254 261
255 262 if captcha.active:
256 263 response = submit(
257 264 self.request.params.get('recaptcha_challenge_field'),
258 265 self.request.params.get('recaptcha_response_field'),
259 266 private_key=captcha.private_key,
260 267 remoteip=get_ip_addr(self.request.environ))
261 268 if not response.is_valid:
262 269 _value = form_result
263 270 _msg = _('Bad captcha')
264 271 error_dict = {'recaptcha_field': _msg}
265 272 raise formencode.Invalid(_msg, _value, None,
266 273 error_dict=error_dict)
267 274
268 275 new_user = UserModel().create_registration(form_result)
269 276 event = UserRegistered(user=new_user, session=self.session)
270 277 self.request.registry.notify(event)
271 278 self.session.flash(
272 279 _('You have successfully registered with RhodeCode'),
273 280 queue='success')
274 281 Session().commit()
275 282
276 283 redirect_ro = self.request.route_path('login')
277 284 raise HTTPFound(redirect_ro)
278 285
279 286 except formencode.Invalid as errors:
280 287 errors.value.pop('password', None)
281 288 errors.value.pop('password_confirmation', None)
282 289 return self.register(
283 290 defaults=errors.value, errors=errors.error_dict)
284 291
285 292 except UserCreationError as e:
286 293 # container auth or other auth functions that create users on
287 294 # the fly can throw this exception signaling that there's issue
288 295 # with user creation, explanation should be provided in
289 296 # Exception itself
290 297 self.session.flash(e, queue='error')
291 298 return self.register()
292 299
293 300 @view_config(
294 301 route_name='reset_password', request_method=('GET', 'POST'),
295 302 renderer='rhodecode:templates/password_reset.mako')
296 303 def password_reset(self):
297 304 captcha = self._get_captcha_data()
298 305
299 306 render_ctx = {
300 307 'captcha_active': captcha.active,
301 308 'captcha_public_key': captcha.public_key,
302 309 'defaults': {},
303 310 'errors': {},
304 311 }
305 312
306 313 # always send implicit message to prevent from discovery of
307 314 # matching emails
308 315 msg = _('If such email exists, a password reset link was sent to it.')
309 316
310 317 if self.request.POST:
311 318 if h.HasPermissionAny('hg.password_reset.disabled')():
312 319 _email = self.request.POST.get('email', '')
313 320 log.error('Failed attempt to reset password for `%s`.', _email)
314 321 self.session.flash(_('Password reset has been disabled.'),
315 322 queue='error')
316 323 return HTTPFound(self.request.route_path('reset_password'))
317 324
318 325 password_reset_form = PasswordResetForm()()
319 326 try:
320 327 form_result = password_reset_form.to_python(
321 328 self.request.params)
322 329 user_email = form_result['email']
323 330
324 331 if captcha.active:
325 332 response = submit(
326 333 self.request.params.get('recaptcha_challenge_field'),
327 334 self.request.params.get('recaptcha_response_field'),
328 335 private_key=captcha.private_key,
329 336 remoteip=get_ip_addr(self.request.environ))
330 337 if not response.is_valid:
331 338 _value = form_result
332 339 _msg = _('Bad captcha')
333 340 error_dict = {'recaptcha_field': _msg}
334 341 raise formencode.Invalid(
335 342 _msg, _value, None, error_dict=error_dict)
336 343
337 344 # Generate reset URL and send mail.
338 345 user = User.get_by_email(user_email)
339 346
340 347 # generate password reset token that expires in 10minutes
341 348 desc = 'Generated token for password reset from {}'.format(
342 349 datetime.datetime.now().isoformat())
343 350 reset_token = AuthTokenModel().create(
344 351 user, lifetime=10,
345 352 description=desc,
346 353 role=UserApiKeys.ROLE_PASSWORD_RESET)
347 354 Session().commit()
348 355
349 356 log.debug('Successfully created password recovery token')
350 357 password_reset_url = self.request.route_url(
351 358 'reset_password_confirmation',
352 359 _query={'key': reset_token.api_key})
353 360 UserModel().reset_password_link(
354 361 form_result, password_reset_url)
355 362 # Display success message and redirect.
356 363 self.session.flash(msg, queue='success')
357 364
365 action_data = {'email': user_email,
366 'user_agent': self.request.user_agent}
358 367 audit_logger.store(action='user.password.reset_request',
359 action_data={'email': user_email},
368 action_data=action_data,
360 369 user=self._rhodecode_user, commit=True)
361 370 return HTTPFound(self.request.route_path('reset_password'))
362 371
363 372 except formencode.Invalid as errors:
364 373 render_ctx.update({
365 374 'defaults': errors.value,
366 375 'errors': errors.error_dict,
367 376 })
368 377 if not self.request.params.get('email'):
369 378 # case of empty email, we want to report that
370 379 return render_ctx
371 380
372 381 if 'recaptcha_field' in errors.error_dict:
373 382 # case of failed captcha
374 383 return render_ctx
375 384
376 385 log.debug('faking response on invalid password reset')
377 386 # make this take 2s, to prevent brute forcing.
378 387 time.sleep(2)
379 388 self.session.flash(msg, queue='success')
380 389 return HTTPFound(self.request.route_path('reset_password'))
381 390
382 391 return render_ctx
383 392
384 393 @view_config(route_name='reset_password_confirmation',
385 394 request_method='GET')
386 395 def password_reset_confirmation(self):
387 396
388 397 if self.request.GET and self.request.GET.get('key'):
389 398 # make this take 2s, to prevent brute forcing.
390 399 time.sleep(2)
391 400
392 401 token = AuthTokenModel().get_auth_token(
393 402 self.request.GET.get('key'))
394 403
395 404 # verify token is the correct role
396 405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
397 406 log.debug('Got token with role:%s expected is %s',
398 407 getattr(token, 'role', 'EMPTY_TOKEN'),
399 408 UserApiKeys.ROLE_PASSWORD_RESET)
400 409 self.session.flash(
401 410 _('Given reset token is invalid'), queue='error')
402 411 return HTTPFound(self.request.route_path('reset_password'))
403 412
404 413 try:
405 414 owner = token.user
406 415 data = {'email': owner.email, 'token': token.api_key}
407 416 UserModel().reset_password(data)
408 417 self.session.flash(
409 418 _('Your password reset was successful, '
410 419 'a new password has been sent to your email'),
411 420 queue='success')
412 421 except Exception as e:
413 422 log.error(e)
414 423 return HTTPFound(self.request.route_path('reset_password'))
415 424
416 425 return HTTPFound(self.request.route_path('login'))
General Comments 0
You need to be logged in to leave comments. Login now