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