##// END OF EJS Templates
exc-tracker: make context attribute behave like in API case...
marcink -
r4277:0d8d4107 default
parent child Browse files
Show More
@@ -1,616 +1,617 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 38 from rhodecode.apps._base import TemplateArgs
39 39 from rhodecode.authentication.base import VCS_TYPE
40 40 from rhodecode.lib import auth, utils2
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 43 from rhodecode.lib.exceptions import UserCreationError
44 44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
45 45 from rhodecode.lib.utils2 import (
46 46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
47 47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
48 48 from rhodecode.model.notification import NotificationModel
49 49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def _filter_proxy(ip):
55 55 """
56 56 Passed in IP addresses in HEADERS can be in a special format of multiple
57 57 ips. Those comma separated IPs are passed from various proxies in the
58 58 chain of request processing. The left-most being the original client.
59 59 We only care about the first IP which came from the org. client.
60 60
61 61 :param ip: ip string from headers
62 62 """
63 63 if ',' in ip:
64 64 _ips = ip.split(',')
65 65 _first_ip = _ips[0].strip()
66 66 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
67 67 return _first_ip
68 68 return ip
69 69
70 70
71 71 def _filter_port(ip):
72 72 """
73 73 Removes a port from ip, there are 4 main cases to handle here.
74 74 - ipv4 eg. 127.0.0.1
75 75 - ipv6 eg. ::1
76 76 - ipv4+port eg. 127.0.0.1:8080
77 77 - ipv6+port eg. [::1]:8080
78 78
79 79 :param ip:
80 80 """
81 81 def is_ipv6(ip_addr):
82 82 if hasattr(socket, 'inet_pton'):
83 83 try:
84 84 socket.inet_pton(socket.AF_INET6, ip_addr)
85 85 except socket.error:
86 86 return False
87 87 else:
88 88 # fallback to ipaddress
89 89 try:
90 90 ipaddress.IPv6Address(safe_unicode(ip_addr))
91 91 except Exception:
92 92 return False
93 93 return True
94 94
95 95 if ':' not in ip: # must be ipv4 pure ip
96 96 return ip
97 97
98 98 if '[' in ip and ']' in ip: # ipv6 with port
99 99 return ip.split(']')[0][1:].lower()
100 100
101 101 # must be ipv6 or ipv4 with port
102 102 if is_ipv6(ip):
103 103 return ip
104 104 else:
105 105 ip, _port = ip.split(':')[:2] # means ipv4+port
106 106 return ip
107 107
108 108
109 109 def get_ip_addr(environ):
110 110 proxy_key = 'HTTP_X_REAL_IP'
111 111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
112 112 def_key = 'REMOTE_ADDR'
113 113 _filters = lambda x: _filter_port(_filter_proxy(x))
114 114
115 115 ip = environ.get(proxy_key)
116 116 if ip:
117 117 return _filters(ip)
118 118
119 119 ip = environ.get(proxy_key2)
120 120 if ip:
121 121 return _filters(ip)
122 122
123 123 ip = environ.get(def_key, '0.0.0.0')
124 124 return _filters(ip)
125 125
126 126
127 127 def get_server_ip_addr(environ, log_errors=True):
128 128 hostname = environ.get('SERVER_NAME')
129 129 try:
130 130 return socket.gethostbyname(hostname)
131 131 except Exception as e:
132 132 if log_errors:
133 133 # in some cases this lookup is not possible, and we don't want to
134 134 # make it an exception in logs
135 135 log.exception('Could not retrieve server ip address: %s', e)
136 136 return hostname
137 137
138 138
139 139 def get_server_port(environ):
140 140 return environ.get('SERVER_PORT')
141 141
142 142
143 143 def get_access_path(environ):
144 144 path = environ.get('PATH_INFO')
145 145 org_req = environ.get('pylons.original_request')
146 146 if org_req:
147 147 path = org_req.environ.get('PATH_INFO')
148 148 return path
149 149
150 150
151 151 def get_user_agent(environ):
152 152 return environ.get('HTTP_USER_AGENT')
153 153
154 154
155 155 def vcs_operation_context(
156 156 environ, repo_name, username, action, scm, check_locking=True,
157 157 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
158 158 """
159 159 Generate the context for a vcs operation, e.g. push or pull.
160 160
161 161 This context is passed over the layers so that hooks triggered by the
162 162 vcs operation know details like the user, the user's IP address etc.
163 163
164 164 :param check_locking: Allows to switch of the computation of the locking
165 165 data. This serves mainly the need of the simplevcs middleware to be
166 166 able to disable this for certain operations.
167 167
168 168 """
169 169 # Tri-state value: False: unlock, None: nothing, True: lock
170 170 make_lock = None
171 171 locked_by = [None, None, None]
172 172 is_anonymous = username == User.DEFAULT_USER
173 173 user = User.get_by_username(username)
174 174 if not is_anonymous and check_locking:
175 175 log.debug('Checking locking on repository "%s"', repo_name)
176 176 repo = Repository.get_by_repo_name(repo_name)
177 177 make_lock, __, locked_by = repo.get_locking_state(
178 178 action, user.user_id)
179 179 user_id = user.user_id
180 180 settings_model = VcsSettingsModel(repo=repo_name)
181 181 ui_settings = settings_model.get_ui_settings()
182 182
183 183 # NOTE(marcink): This should be also in sync with
184 184 # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data
185 185 store = [x for x in ui_settings if x.key == '/']
186 186 repo_store = ''
187 187 if store:
188 188 repo_store = store[0].value
189 189
190 190 scm_data = {
191 191 'ip': get_ip_addr(environ),
192 192 'username': username,
193 193 'user_id': user_id,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'repo_store': repo_store,
199 199 'make_lock': make_lock,
200 200 'locked_by': locked_by,
201 201 'server_url': utils2.get_server_url(environ),
202 202 'user_agent': get_user_agent(environ),
203 203 'hooks': get_enabled_hook_classes(ui_settings),
204 204 'is_shadow_repo': is_shadow_repo,
205 205 'detect_force_push': detect_force_push,
206 206 'check_branch_perms': check_branch_perms,
207 207 }
208 208 return scm_data
209 209
210 210
211 211 class BasicAuth(AuthBasicAuthenticator):
212 212
213 213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
214 214 initial_call_detection=False, acl_repo_name=None, rc_realm=''):
215 215 self.realm = realm
216 216 self.rc_realm = rc_realm
217 217 self.initial_call = initial_call_detection
218 218 self.authfunc = authfunc
219 219 self.registry = registry
220 220 self.acl_repo_name = acl_repo_name
221 221 self._rc_auth_http_code = auth_http_code
222 222
223 223 def _get_response_from_code(self, http_code):
224 224 try:
225 225 return get_exception(safe_int(http_code))
226 226 except Exception:
227 227 log.exception('Failed to fetch response for code %s', http_code)
228 228 return HTTPForbidden
229 229
230 230 def get_rc_realm(self):
231 231 return safe_str(self.rc_realm)
232 232
233 233 def build_authentication(self):
234 234 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
235 235 if self._rc_auth_http_code and not self.initial_call:
236 236 # return alternative HTTP code if alternative http return code
237 237 # is specified in RhodeCode config, but ONLY if it's not the
238 238 # FIRST call
239 239 custom_response_klass = self._get_response_from_code(
240 240 self._rc_auth_http_code)
241 241 return custom_response_klass(headers=head)
242 242 return HTTPUnauthorized(headers=head)
243 243
244 244 def authenticate(self, environ):
245 245 authorization = AUTHORIZATION(environ)
246 246 if not authorization:
247 247 return self.build_authentication()
248 248 (authmeth, auth) = authorization.split(' ', 1)
249 249 if 'basic' != authmeth.lower():
250 250 return self.build_authentication()
251 251 auth = auth.strip().decode('base64')
252 252 _parts = auth.split(':', 1)
253 253 if len(_parts) == 2:
254 254 username, password = _parts
255 255 auth_data = self.authfunc(
256 256 username, password, environ, VCS_TYPE,
257 257 registry=self.registry, acl_repo_name=self.acl_repo_name)
258 258 if auth_data:
259 259 return {'username': username, 'auth_data': auth_data}
260 260 if username and password:
261 261 # we mark that we actually executed authentication once, at
262 262 # that point we can use the alternative auth code
263 263 self.initial_call = False
264 264
265 265 return self.build_authentication()
266 266
267 267 __call__ = authenticate
268 268
269 269
270 270 def calculate_version_hash(config):
271 271 return sha1(
272 272 config.get('beaker.session.secret', '') +
273 273 rhodecode.__version__)[:8]
274 274
275 275
276 276 def get_current_lang(request):
277 277 # NOTE(marcink): remove after pyramid move
278 278 try:
279 279 return translation.get_lang()[0]
280 280 except:
281 281 pass
282 282
283 283 return getattr(request, '_LOCALE_', request.locale_name)
284 284
285 285
286 def attach_context_attributes(context, request, user_id=None):
286 def attach_context_attributes(context, request, user_id=None, is_api=None):
287 287 """
288 288 Attach variables into template context called `c`.
289 289 """
290 290 config = request.registry.settings
291 291
292 292 rc_config = SettingsModel().get_all_settings(cache=True, from_request=False)
293 293 context.rc_config = rc_config
294 294 context.rhodecode_version = rhodecode.__version__
295 295 context.rhodecode_edition = config.get('rhodecode.edition')
296 296 # unique secret + version does not leak the version but keep consistency
297 297 context.rhodecode_version_hash = calculate_version_hash(config)
298 298
299 299 # Default language set for the incoming request
300 300 context.language = get_current_lang(request)
301 301
302 302 # Visual options
303 303 context.visual = AttributeDict({})
304 304
305 305 # DB stored Visual Items
306 306 context.visual.show_public_icon = str2bool(
307 307 rc_config.get('rhodecode_show_public_icon'))
308 308 context.visual.show_private_icon = str2bool(
309 309 rc_config.get('rhodecode_show_private_icon'))
310 310 context.visual.stylify_metatags = str2bool(
311 311 rc_config.get('rhodecode_stylify_metatags'))
312 312 context.visual.dashboard_items = safe_int(
313 313 rc_config.get('rhodecode_dashboard_items', 100))
314 314 context.visual.admin_grid_items = safe_int(
315 315 rc_config.get('rhodecode_admin_grid_items', 100))
316 316 context.visual.show_revision_number = str2bool(
317 317 rc_config.get('rhodecode_show_revision_number', True))
318 318 context.visual.show_sha_length = safe_int(
319 319 rc_config.get('rhodecode_show_sha_length', 100))
320 320 context.visual.repository_fields = str2bool(
321 321 rc_config.get('rhodecode_repository_fields'))
322 322 context.visual.show_version = str2bool(
323 323 rc_config.get('rhodecode_show_version'))
324 324 context.visual.use_gravatar = str2bool(
325 325 rc_config.get('rhodecode_use_gravatar'))
326 326 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
327 327 context.visual.default_renderer = rc_config.get(
328 328 'rhodecode_markup_renderer', 'rst')
329 329 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
330 330 context.visual.rhodecode_support_url = \
331 331 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
332 332
333 333 context.visual.affected_files_cut_off = 60
334 334
335 335 context.pre_code = rc_config.get('rhodecode_pre_code')
336 336 context.post_code = rc_config.get('rhodecode_post_code')
337 337 context.rhodecode_name = rc_config.get('rhodecode_title')
338 338 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
339 339 # if we have specified default_encoding in the request, it has more
340 340 # priority
341 341 if request.GET.get('default_encoding'):
342 342 context.default_encodings.insert(0, request.GET.get('default_encoding'))
343 343 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
344 344 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
345 345
346 346 # INI stored
347 347 context.labs_active = str2bool(
348 348 config.get('labs_settings_active', 'false'))
349 349 context.ssh_enabled = str2bool(
350 350 config.get('ssh.generate_authorized_keyfile', 'false'))
351 351 context.ssh_key_generator_enabled = str2bool(
352 352 config.get('ssh.enable_ui_key_generator', 'true'))
353 353
354 354 context.visual.allow_repo_location_change = str2bool(
355 355 config.get('allow_repo_location_change', True))
356 356 context.visual.allow_custom_hooks_settings = str2bool(
357 357 config.get('allow_custom_hooks_settings', True))
358 358 context.debug_style = str2bool(config.get('debug_style', False))
359 359
360 360 context.rhodecode_instanceid = config.get('instance_id')
361 361
362 362 context.visual.cut_off_limit_diff = safe_int(
363 363 config.get('cut_off_limit_diff'))
364 364 context.visual.cut_off_limit_file = safe_int(
365 365 config.get('cut_off_limit_file'))
366 366
367 367 context.license = AttributeDict({})
368 368 context.license.hide_license_info = str2bool(
369 369 config.get('license.hide_license_info', False))
370 370
371 371 # AppEnlight
372 372 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
373 373 context.appenlight_api_public_key = config.get(
374 374 'appenlight.api_public_key', '')
375 375 context.appenlight_server_url = config.get('appenlight.server_url', '')
376 376
377 377 diffmode = {
378 378 "unified": "unified",
379 379 "sideside": "sideside"
380 380 }.get(request.GET.get('diffmode'))
381 381
382 if is_api is not None:
382 383 is_api = hasattr(request, 'rpc_user')
383 384 session_attrs = {
384 385 # defaults
385 386 "clone_url_format": "http",
386 387 "diffmode": "sideside"
387 388 }
388 389
389 390 if not is_api:
390 391 # don't access pyramid session for API calls
391 392 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
392 393 request.session['rc_user_session_attr.diffmode'] = diffmode
393 394
394 395 # session settings per user
395 396
396 397 for k, v in request.session.items():
397 398 pref = 'rc_user_session_attr.'
398 399 if k and k.startswith(pref):
399 400 k = k[len(pref):]
400 401 session_attrs[k] = v
401 402
402 403 context.user_session_attrs = session_attrs
403 404
404 405 # JS template context
405 406 context.template_context = {
406 407 'repo_name': None,
407 408 'repo_type': None,
408 409 'repo_landing_commit': None,
409 410 'rhodecode_user': {
410 411 'username': None,
411 412 'email': None,
412 413 'notification_status': False
413 414 },
414 415 'session_attrs': session_attrs,
415 416 'visual': {
416 417 'default_renderer': None
417 418 },
418 419 'commit_data': {
419 420 'commit_id': None
420 421 },
421 422 'pull_request_data': {'pull_request_id': None},
422 423 'timeago': {
423 424 'refresh_time': 120 * 1000,
424 425 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
425 426 },
426 427 'pyramid_dispatch': {
427 428
428 429 },
429 430 'extra': {'plugins': {}}
430 431 }
431 432 # END CONFIG VARS
432 433 if is_api:
433 434 csrf_token = None
434 435 else:
435 436 csrf_token = auth.get_csrf_token(session=request.session)
436 437
437 438 context.csrf_token = csrf_token
438 439 context.backends = rhodecode.BACKENDS.keys()
439 440 context.backends.sort()
440 441 unread_count = 0
441 442 user_bookmark_list = []
442 443 if user_id:
443 444 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
444 445 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
445 446 context.unread_notifications = unread_count
446 447 context.bookmark_items = user_bookmark_list
447 448
448 449 # web case
449 450 if hasattr(request, 'user'):
450 451 context.auth_user = request.user
451 452 context.rhodecode_user = request.user
452 453
453 454 # api case
454 455 if hasattr(request, 'rpc_user'):
455 456 context.auth_user = request.rpc_user
456 457 context.rhodecode_user = request.rpc_user
457 458
458 459 # attach the whole call context to the request
459 460 request.call_context = context
460 461
461 462
462 463 def get_auth_user(request):
463 464 environ = request.environ
464 465 session = request.session
465 466
466 467 ip_addr = get_ip_addr(environ)
467 468
468 469 # make sure that we update permissions each time we call controller
469 470 _auth_token = (request.GET.get('auth_token', '') or request.GET.get('api_key', ''))
470 471 if not _auth_token and request.matchdict:
471 472 url_auth_token = request.matchdict.get('_auth_token')
472 473 _auth_token = url_auth_token
473 474 if _auth_token:
474 475 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
475 476
476 477 if _auth_token:
477 478 # when using API_KEY we assume user exists, and
478 479 # doesn't need auth based on cookies.
479 480 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
480 481 authenticated = False
481 482 else:
482 483 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
483 484 try:
484 485 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
485 486 ip_addr=ip_addr)
486 487 except UserCreationError as e:
487 488 h.flash(e, 'error')
488 489 # container auth or other auth functions that create users
489 490 # on the fly can throw this exception signaling that there's
490 491 # issue with user creation, explanation should be provided
491 492 # in Exception itself. We then create a simple blank
492 493 # AuthUser
493 494 auth_user = AuthUser(ip_addr=ip_addr)
494 495
495 496 # in case someone changes a password for user it triggers session
496 497 # flush and forces a re-login
497 498 if password_changed(auth_user, session):
498 499 session.invalidate()
499 500 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
500 501 auth_user = AuthUser(ip_addr=ip_addr)
501 502
502 503 authenticated = cookie_store.get('is_authenticated')
503 504
504 505 if not auth_user.is_authenticated and auth_user.is_user_object:
505 506 # user is not authenticated and not empty
506 507 auth_user.set_authenticated(authenticated)
507 508
508 509 return auth_user, _auth_token
509 510
510 511
511 512 def h_filter(s):
512 513 """
513 514 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
514 515 we wrap this with additional functionality that converts None to empty
515 516 strings
516 517 """
517 518 if s is None:
518 519 return markupsafe.Markup()
519 520 return markupsafe.escape(s)
520 521
521 522
522 523 def add_events_routes(config):
523 524 """
524 525 Adds routing that can be used in events. Because some events are triggered
525 526 outside of pyramid context, we need to bootstrap request with some
526 527 routing registered
527 528 """
528 529
529 530 from rhodecode.apps._base import ADMIN_PREFIX
530 531
531 532 config.add_route(name='home', pattern='/')
532 533 config.add_route(name='main_page_repos_data', pattern='/_home_repos')
533 534 config.add_route(name='main_page_repo_groups_data', pattern='/_home_repo_groups')
534 535
535 536 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
536 537 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
537 538 config.add_route(name='repo_summary', pattern='/{repo_name}')
538 539 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
539 540 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
540 541
541 542 config.add_route(name='pullrequest_show',
542 543 pattern='/{repo_name}/pull-request/{pull_request_id}')
543 544 config.add_route(name='pull_requests_global',
544 545 pattern='/pull-request/{pull_request_id}')
545 546
546 547 config.add_route(name='repo_commit',
547 548 pattern='/{repo_name}/changeset/{commit_id}')
548 549 config.add_route(name='repo_files',
549 550 pattern='/{repo_name}/files/{commit_id}/{f_path}')
550 551
551 552 config.add_route(name='hovercard_user',
552 553 pattern='/_hovercard/user/{user_id}')
553 554
554 555 config.add_route(name='hovercard_user_group',
555 556 pattern='/_hovercard/user_group/{user_group_id}')
556 557
557 558 config.add_route(name='hovercard_pull_request',
558 559 pattern='/_hovercard/pull_request/{pull_request_id}')
559 560
560 561 config.add_route(name='hovercard_repo_commit',
561 562 pattern='/_hovercard/commit/{repo_name}/{commit_id}')
562 563
563 564
564 565 def bootstrap_config(request):
565 566 import pyramid.testing
566 567 registry = pyramid.testing.Registry('RcTestRegistry')
567 568
568 569 config = pyramid.testing.setUp(registry=registry, request=request)
569 570
570 571 # allow pyramid lookup in testing
571 572 config.include('pyramid_mako')
572 573 config.include('rhodecode.lib.rc_beaker')
573 574 config.include('rhodecode.lib.rc_cache')
574 575
575 576 add_events_routes(config)
576 577
577 578 return config
578 579
579 580
580 581 def bootstrap_request(**kwargs):
581 582 import pyramid.testing
582 583
583 584 class TestRequest(pyramid.testing.DummyRequest):
584 585 application_url = kwargs.pop('application_url', 'http://example.com')
585 586 host = kwargs.pop('host', 'example.com:80')
586 587 domain = kwargs.pop('domain', 'example.com')
587 588
588 589 def translate(self, msg):
589 590 return msg
590 591
591 592 def plularize(self, singular, plural, n):
592 593 return singular
593 594
594 595 def get_partial_renderer(self, tmpl_name):
595 596
596 597 from rhodecode.lib.partial_renderer import get_partial_renderer
597 598 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
598 599
599 600 _call_context = TemplateArgs()
600 601 _call_context.visual = TemplateArgs()
601 602 _call_context.visual.show_sha_length = 12
602 603 _call_context.visual.show_revision_number = True
603 604
604 605 @property
605 606 def call_context(self):
606 607 return self._call_context
607 608
608 609 class TestDummySession(pyramid.testing.DummySession):
609 610 def save(*arg, **kw):
610 611 pass
611 612
612 613 request = TestRequest(**kwargs)
613 614 request.session = TestDummySession()
614 615
615 616 return request
616 617
@@ -1,217 +1,220 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 os
22 22 import time
23 23 import datetime
24 24 import msgpack
25 25 import logging
26 26 import traceback
27 27 import tempfile
28 28 import glob
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
33 33 global_prefix = 'rhodecode'
34 34 exc_store_dir_name = 'rc_exception_store_v1'
35 35
36 36
37 37 def exc_serialize(exc_id, tb, exc_type):
38 38
39 39 data = {
40 40 'version': 'v1',
41 41 'exc_id': exc_id,
42 42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 43 'exc_timestamp': repr(time.time()),
44 44 'exc_message': tb,
45 45 'exc_type': exc_type,
46 46 }
47 47 return msgpack.packb(data), data
48 48
49 49
50 50 def exc_unserialize(tb):
51 51 return msgpack.unpackb(tb)
52 52
53 53 _exc_store = None
54 54
55 55
56 56 def get_exc_store():
57 57 """
58 58 Get and create exception store if it's not existing
59 59 """
60 60 global _exc_store
61 61 import rhodecode as app
62 62
63 63 if _exc_store is not None:
64 64 # quick global cache
65 65 return _exc_store
66 66
67 67 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
68 68 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
69 69
70 70 _exc_store_path = os.path.abspath(_exc_store_path)
71 71 if not os.path.isdir(_exc_store_path):
72 72 os.makedirs(_exc_store_path)
73 73 log.debug('Initializing exceptions store at %s', _exc_store_path)
74 74 _exc_store = _exc_store_path
75 75
76 76 return _exc_store_path
77 77
78 78
79 79 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None):
80 80 """
81 81 Low level function to store exception in the exception tracker
82 82 """
83 83 import rhodecode as app
84 84
85 85 exc_store_path = get_exc_store()
86 86 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name)
87 87 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
88 88 if not os.path.isdir(exc_store_path):
89 89 os.makedirs(exc_store_path)
90 90 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
91 91 with open(stored_exc_path, 'wb') as f:
92 92 f.write(exc_data)
93 93 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
94 94
95 95 if send_email is None:
96 96 # NOTE(marcink): read app config unless we specify explicitly
97 97 send_email = app.CONFIG.get('exception_tracker.send_email', False)
98 98
99 99 if send_email:
100 100 try:
101 101 send_exc_email(exc_id, exc_type_name)
102 102 except Exception:
103 103 log.exception('Failed to send exception email')
104 104 pass
105 105
106 106
107 107 def send_exc_email(exc_id, exc_type_name):
108 108 import rhodecode as app
109 109 from pyramid.threadlocal import get_current_request
110 110 from rhodecode.apps._base import TemplateArgs
111 111 from rhodecode.lib.utils2 import aslist
112 112 from rhodecode.lib.celerylib import run_task, tasks
113 113 from rhodecode.lib.base import attach_context_attributes
114 114 from rhodecode.model.notification import EmailNotificationModel
115 115
116 116 request = get_current_request()
117 117
118 118 recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', ''))
119 119 log.debug('Sending Email exception to: `%s`', recipients or 'all super admins')
120 120
121 121 # NOTE(marcink): needed for email template rendering
122 attach_context_attributes(TemplateArgs(), request, request.user.user_id)
122 user_id = None
123 if request:
124 user_id = request.user.user_id
125 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
123 126
124 127 email_kwargs = {
125 128 'email_prefix': app.CONFIG.get('exception_tracker.email_prefix', '') or '[RHODECODE ERROR]',
126 129 'exc_url': request.route_url('admin_settings_exception_tracker_show', exception_id=exc_id),
127 130 'exc_id': exc_id,
128 131 'exc_type_name': exc_type_name,
129 132 'exc_traceback': read_exception(exc_id, prefix=None),
130 133 }
131 134
132 135 (subject, headers, email_body,
133 136 email_body_plaintext) = EmailNotificationModel().render_email(
134 137 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
135 138
136 139 run_task(tasks.send_email, recipients, subject,
137 140 email_body_plaintext, email_body)
138 141
139 142
140 143 def _prepare_exception(exc_info):
141 144 exc_type, exc_value, exc_traceback = exc_info
142 145 exc_type_name = exc_type.__name__
143 146
144 147 tb = ''.join(traceback.format_exception(
145 148 exc_type, exc_value, exc_traceback, None))
146 149
147 150 return exc_type_name, tb
148 151
149 152
150 153 def store_exception(exc_id, exc_info, prefix=global_prefix):
151 154 """
152 155 Example usage::
153 156
154 157 exc_info = sys.exc_info()
155 158 store_exception(id(exc_info), exc_info)
156 159 """
157 160
158 161 try:
159 162 exc_type_name, exc_traceback = _prepare_exception(exc_info)
160 163 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
161 164 exc_traceback=exc_traceback, prefix=prefix)
162 165 return exc_id, exc_type_name
163 166 except Exception:
164 167 log.exception('Failed to store exception `%s` information', exc_id)
165 168 # there's no way this can fail, it will crash server badly if it does.
166 169 pass
167 170
168 171
169 172 def _find_exc_file(exc_id, prefix=global_prefix):
170 173 exc_store_path = get_exc_store()
171 174 if prefix:
172 175 exc_id = '{}_{}'.format(exc_id, prefix)
173 176 else:
174 177 # search without a prefix
175 178 exc_id = '{}'.format(exc_id)
176 179
177 180 found_exc_id = None
178 181 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
179 182 if matches:
180 183 found_exc_id = matches[0]
181 184
182 185 return found_exc_id
183 186
184 187
185 188 def _read_exception(exc_id, prefix):
186 189 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
187 190 if exc_id_file_path:
188 191 with open(exc_id_file_path, 'rb') as f:
189 192 return exc_unserialize(f.read())
190 193 else:
191 194 log.debug('Exception File `%s` not found', exc_id_file_path)
192 195 return None
193 196
194 197
195 198 def read_exception(exc_id, prefix=global_prefix):
196 199 try:
197 200 return _read_exception(exc_id=exc_id, prefix=prefix)
198 201 except Exception:
199 202 log.exception('Failed to read exception `%s` information', exc_id)
200 203 # there's no way this can fail, it will crash server badly if it does.
201 204 return None
202 205
203 206
204 207 def delete_exception(exc_id, prefix=global_prefix):
205 208 try:
206 209 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
207 210 if exc_id_file_path:
208 211 os.remove(exc_id_file_path)
209 212
210 213 except Exception:
211 214 log.exception('Failed to remove exception `%s` information', exc_id)
212 215 # there's no way this can fail, it will crash server badly if it does.
213 216 pass
214 217
215 218
216 219 def generate_id():
217 220 return id(object())
General Comments 0
You need to be logged in to leave comments. Login now