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