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