##// END OF EJS Templates
core: improve attach context attribute to rely strictly on already provided user.
marcink -
r1776:e85e3fbe default
parent child Browse files
Show More
@@ -1,594 +1,592 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
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 ipaddress
31 31 import pyramid.threadlocal
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 from pylons import config, tmpl_context as c, request, session, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(ip_addr)
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def get_user_agent(environ):
166 166 return environ.get('HTTP_USER_AGENT')
167 167
168 168
169 169 def vcs_operation_context(
170 170 environ, repo_name, username, action, scm, check_locking=True,
171 171 is_shadow_repo=False):
172 172 """
173 173 Generate the context for a vcs operation, e.g. push or pull.
174 174
175 175 This context is passed over the layers so that hooks triggered by the
176 176 vcs operation know details like the user, the user's IP address etc.
177 177
178 178 :param check_locking: Allows to switch of the computation of the locking
179 179 data. This serves mainly the need of the simplevcs middleware to be
180 180 able to disable this for certain operations.
181 181
182 182 """
183 183 # Tri-state value: False: unlock, None: nothing, True: lock
184 184 make_lock = None
185 185 locked_by = [None, None, None]
186 186 is_anonymous = username == User.DEFAULT_USER
187 187 if not is_anonymous and check_locking:
188 188 log.debug('Checking locking on repository "%s"', repo_name)
189 189 user = User.get_by_username(username)
190 190 repo = Repository.get_by_repo_name(repo_name)
191 191 make_lock, __, locked_by = repo.get_locking_state(
192 192 action, user.user_id)
193 193
194 194 settings_model = VcsSettingsModel(repo=repo_name)
195 195 ui_settings = settings_model.get_ui_settings()
196 196
197 197 extras = {
198 198 'ip': get_ip_addr(environ),
199 199 'username': username,
200 200 'action': action,
201 201 'repository': repo_name,
202 202 'scm': scm,
203 203 'config': rhodecode.CONFIG['__file__'],
204 204 'make_lock': make_lock,
205 205 'locked_by': locked_by,
206 206 'server_url': utils2.get_server_url(environ),
207 207 'user_agent': get_user_agent(environ),
208 208 'hooks': get_enabled_hook_classes(ui_settings),
209 209 'is_shadow_repo': is_shadow_repo,
210 210 }
211 211 return extras
212 212
213 213
214 214 class BasicAuth(AuthBasicAuthenticator):
215 215
216 216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 217 initial_call_detection=False, acl_repo_name=None):
218 218 self.realm = realm
219 219 self.initial_call = initial_call_detection
220 220 self.authfunc = authfunc
221 221 self.registry = registry
222 222 self.acl_repo_name = acl_repo_name
223 223 self._rc_auth_http_code = auth_http_code
224 224
225 225 def _get_response_from_code(self, http_code):
226 226 try:
227 227 return get_exception(safe_int(http_code))
228 228 except Exception:
229 229 log.exception('Failed to fetch response for code %s' % http_code)
230 230 return HTTPForbidden
231 231
232 232 def build_authentication(self):
233 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 234 if self._rc_auth_http_code and not self.initial_call:
235 235 # return alternative HTTP code if alternative http return code
236 236 # is specified in RhodeCode config, but ONLY if it's not the
237 237 # FIRST call
238 238 custom_response_klass = self._get_response_from_code(
239 239 self._rc_auth_http_code)
240 240 return custom_response_klass(headers=head)
241 241 return HTTPUnauthorized(headers=head)
242 242
243 243 def authenticate(self, environ):
244 244 authorization = AUTHORIZATION(environ)
245 245 if not authorization:
246 246 return self.build_authentication()
247 247 (authmeth, auth) = authorization.split(' ', 1)
248 248 if 'basic' != authmeth.lower():
249 249 return self.build_authentication()
250 250 auth = auth.strip().decode('base64')
251 251 _parts = auth.split(':', 1)
252 252 if len(_parts) == 2:
253 253 username, password = _parts
254 254 if self.authfunc(
255 255 username, password, environ, VCS_TYPE,
256 256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 257 return username
258 258 if username and password:
259 259 # we mark that we actually executed authentication once, at
260 260 # that point we can use the alternative auth code
261 261 self.initial_call = False
262 262
263 263 return self.build_authentication()
264 264
265 265 __call__ = authenticate
266 266
267 267
268 def attach_context_attributes(context, request):
268 def attach_context_attributes(context, request, user_id):
269 269 """
270 270 Attach variables into template context called `c`, please note that
271 271 request could be pylons or pyramid request in here.
272 272 """
273 273 rc_config = SettingsModel().get_all_settings(cache=True)
274 274
275 275 context.rhodecode_version = rhodecode.__version__
276 276 context.rhodecode_edition = config.get('rhodecode.edition')
277 277 # unique secret + version does not leak the version but keep consistency
278 278 context.rhodecode_version_hash = md5(
279 279 config.get('beaker.session.secret', '') +
280 280 rhodecode.__version__)[:8]
281 281
282 282 # Default language set for the incoming request
283 283 context.language = translation.get_lang()[0]
284 284
285 285 # Visual options
286 286 context.visual = AttributeDict({})
287 287
288 288 # DB stored Visual Items
289 289 context.visual.show_public_icon = str2bool(
290 290 rc_config.get('rhodecode_show_public_icon'))
291 291 context.visual.show_private_icon = str2bool(
292 292 rc_config.get('rhodecode_show_private_icon'))
293 293 context.visual.stylify_metatags = str2bool(
294 294 rc_config.get('rhodecode_stylify_metatags'))
295 295 context.visual.dashboard_items = safe_int(
296 296 rc_config.get('rhodecode_dashboard_items', 100))
297 297 context.visual.admin_grid_items = safe_int(
298 298 rc_config.get('rhodecode_admin_grid_items', 100))
299 299 context.visual.repository_fields = str2bool(
300 300 rc_config.get('rhodecode_repository_fields'))
301 301 context.visual.show_version = str2bool(
302 302 rc_config.get('rhodecode_show_version'))
303 303 context.visual.use_gravatar = str2bool(
304 304 rc_config.get('rhodecode_use_gravatar'))
305 305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
306 306 context.visual.default_renderer = rc_config.get(
307 307 'rhodecode_markup_renderer', 'rst')
308 308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
309 309 context.visual.rhodecode_support_url = \
310 310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
311 311
312 312 context.pre_code = rc_config.get('rhodecode_pre_code')
313 313 context.post_code = rc_config.get('rhodecode_post_code')
314 314 context.rhodecode_name = rc_config.get('rhodecode_title')
315 315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
316 316 # if we have specified default_encoding in the request, it has more
317 317 # priority
318 318 if request.GET.get('default_encoding'):
319 319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
320 320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
321 321
322 322 # INI stored
323 323 context.labs_active = str2bool(
324 324 config.get('labs_settings_active', 'false'))
325 325 context.visual.allow_repo_location_change = str2bool(
326 326 config.get('allow_repo_location_change', True))
327 327 context.visual.allow_custom_hooks_settings = str2bool(
328 328 config.get('allow_custom_hooks_settings', True))
329 329 context.debug_style = str2bool(config.get('debug_style', False))
330 330
331 331 context.rhodecode_instanceid = config.get('instance_id')
332 332
333 333 # AppEnlight
334 334 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
335 335 context.appenlight_api_public_key = config.get(
336 336 'appenlight.api_public_key', '')
337 337 context.appenlight_server_url = config.get('appenlight.server_url', '')
338 338
339 339 # JS template context
340 340 context.template_context = {
341 341 'repo_name': None,
342 342 'repo_type': None,
343 343 'repo_landing_commit': None,
344 344 'rhodecode_user': {
345 345 'username': None,
346 346 'email': None,
347 347 'notification_status': False
348 348 },
349 349 'visual': {
350 350 'default_renderer': None
351 351 },
352 352 'commit_data': {
353 353 'commit_id': None
354 354 },
355 355 'pull_request_data': {'pull_request_id': None},
356 356 'timeago': {
357 357 'refresh_time': 120 * 1000,
358 358 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
359 359 },
360 360 'pylons_dispatch': {
361 361 # 'controller': request.environ['pylons.routes_dict']['controller'],
362 362 # 'action': request.environ['pylons.routes_dict']['action'],
363 363 },
364 364 'pyramid_dispatch': {
365 365
366 366 },
367 367 'extra': {'plugins': {}}
368 368 }
369 369 # END CONFIG VARS
370 370
371 371 # TODO: This dosn't work when called from pylons compatibility tween.
372 372 # Fix this and remove it from base controller.
373 373 # context.repo_name = get_repo_slug(request) # can be empty
374 374
375 375 diffmode = 'sideside'
376 376 if request.GET.get('diffmode'):
377 377 if request.GET['diffmode'] == 'unified':
378 378 diffmode = 'unified'
379 379 elif request.session.get('diffmode'):
380 380 diffmode = request.session['diffmode']
381 381
382 382 context.diffmode = diffmode
383 383
384 384 if request.session.get('diffmode') != diffmode:
385 385 request.session['diffmode'] = diffmode
386 386
387 387 context.csrf_token = auth.get_csrf_token()
388 388 context.backends = rhodecode.BACKENDS.keys()
389 389 context.backends.sort()
390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
391 context.rhodecode_user.user_id)
392
390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
393 391 context.pyramid_request = pyramid.threadlocal.get_current_request()
394 392
395 393
396 394 def get_auth_user(environ):
397 395 ip_addr = get_ip_addr(environ)
398 396 # make sure that we update permissions each time we call controller
399 397 _auth_token = (request.GET.get('auth_token', '') or
400 398 request.GET.get('api_key', ''))
401 399
402 400 if _auth_token:
403 401 # when using API_KEY we assume user exists, and
404 402 # doesn't need auth based on cookies.
405 403 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
406 404 authenticated = False
407 405 else:
408 406 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
409 407 try:
410 408 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
411 409 ip_addr=ip_addr)
412 410 except UserCreationError as e:
413 411 h.flash(e, 'error')
414 412 # container auth or other auth functions that create users
415 413 # on the fly can throw this exception signaling that there's
416 414 # issue with user creation, explanation should be provided
417 415 # in Exception itself. We then create a simple blank
418 416 # AuthUser
419 417 auth_user = AuthUser(ip_addr=ip_addr)
420 418
421 419 if password_changed(auth_user, session):
422 420 session.invalidate()
423 421 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
424 422 auth_user = AuthUser(ip_addr=ip_addr)
425 423
426 424 authenticated = cookie_store.get('is_authenticated')
427 425
428 426 if not auth_user.is_authenticated and auth_user.is_user_object:
429 427 # user is not authenticated and not empty
430 428 auth_user.set_authenticated(authenticated)
431 429
432 430 return auth_user
433 431
434 432
435 433 class BaseController(WSGIController):
436 434
437 435 def __before__(self):
438 436 """
439 437 __before__ is called before controller methods and after __call__
440 438 """
441 439 # on each call propagate settings calls into global settings.
442 440 set_rhodecode_config(config)
443 attach_context_attributes(c, request)
441 attach_context_attributes(c, request, c.rhodecode_user.user_id)
444 442
445 443 # TODO: Remove this when fixed in attach_context_attributes()
446 444 c.repo_name = get_repo_slug(request) # can be empty
447 445
448 446 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
449 447 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
450 448 self.sa = meta.Session
451 449 self.scm_model = ScmModel(self.sa)
452 450
453 451 # set user language
454 452 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
455 453 if user_lang:
456 454 translation.set_lang(user_lang)
457 455 log.debug('set language to %s for user %s',
458 456 user_lang, self._rhodecode_user)
459 457
460 458 def _dispatch_redirect(self, with_url, environ, start_response):
461 459 resp = HTTPFound(with_url)
462 460 environ['SCRIPT_NAME'] = '' # handle prefix middleware
463 461 environ['PATH_INFO'] = with_url
464 462 return resp(environ, start_response)
465 463
466 464 def __call__(self, environ, start_response):
467 465 """Invoke the Controller"""
468 466 # WSGIController.__call__ dispatches to the Controller method
469 467 # the request is routed to. This routing information is
470 468 # available in environ['pylons.routes_dict']
471 469 from rhodecode.lib import helpers as h
472 470
473 471 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
474 472 if environ.get('debugtoolbar.wants_pylons_context', False):
475 473 environ['debugtoolbar.pylons_context'] = c._current_obj()
476 474
477 475 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
478 476 environ['pylons.routes_dict']['action']])
479 477
480 478 self.rc_config = SettingsModel().get_all_settings(cache=True)
481 479 self.ip_addr = get_ip_addr(environ)
482 480
483 481 # The rhodecode auth user is looked up and passed through the
484 482 # environ by the pylons compatibility tween in pyramid.
485 483 # So we can just grab it from there.
486 484 auth_user = environ['rc_auth_user']
487 485
488 486 # set globals for auth user
489 487 request.user = auth_user
490 488 c.rhodecode_user = self._rhodecode_user = auth_user
491 489
492 490 log.info('IP: %s User: %s accessed %s [%s]' % (
493 491 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
494 492 _route_name)
495 493 )
496 494
497 495 user_obj = auth_user.get_instance()
498 496 if user_obj and user_obj.user_data.get('force_password_change'):
499 497 h.flash('You are required to change your password', 'warning',
500 498 ignore_duplicate=True)
501 499 return self._dispatch_redirect(
502 500 url('my_account_password'), environ, start_response)
503 501
504 502 return WSGIController.__call__(self, environ, start_response)
505 503
506 504
507 505 class BaseRepoController(BaseController):
508 506 """
509 507 Base class for controllers responsible for loading all needed data for
510 508 repository loaded items are
511 509
512 510 c.rhodecode_repo: instance of scm repository
513 511 c.rhodecode_db_repo: instance of db
514 512 c.repository_requirements_missing: shows that repository specific data
515 513 could not be displayed due to the missing requirements
516 514 c.repository_pull_requests: show number of open pull requests
517 515 """
518 516
519 517 def __before__(self):
520 518 super(BaseRepoController, self).__before__()
521 519 if c.repo_name: # extracted from routes
522 520 db_repo = Repository.get_by_repo_name(c.repo_name)
523 521 if not db_repo:
524 522 return
525 523
526 524 log.debug(
527 525 'Found repository in database %s with state `%s`',
528 526 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
529 527 route = getattr(request.environ.get('routes.route'), 'name', '')
530 528
531 529 # allow to delete repos that are somehow damages in filesystem
532 530 if route in ['delete_repo']:
533 531 return
534 532
535 533 if db_repo.repo_state in [Repository.STATE_PENDING]:
536 534 if route in ['repo_creating_home']:
537 535 return
538 536 check_url = url('repo_creating_home', repo_name=c.repo_name)
539 537 return redirect(check_url)
540 538
541 539 self.rhodecode_db_repo = db_repo
542 540
543 541 missing_requirements = False
544 542 try:
545 543 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
546 544 except RepositoryRequirementError as e:
547 545 missing_requirements = True
548 546 self._handle_missing_requirements(e)
549 547
550 548 if self.rhodecode_repo is None and not missing_requirements:
551 549 log.error('%s this repository is present in database but it '
552 550 'cannot be created as an scm instance', c.repo_name)
553 551
554 552 h.flash(_(
555 553 "The repository at %(repo_name)s cannot be located.") %
556 554 {'repo_name': c.repo_name},
557 555 category='error', ignore_duplicate=True)
558 556 redirect(h.route_path('home'))
559 557
560 558 # update last change according to VCS data
561 559 if not missing_requirements:
562 560 commit = db_repo.get_commit(
563 561 pre_load=["author", "date", "message", "parents"])
564 562 db_repo.update_commit_cache(commit)
565 563
566 564 # Prepare context
567 565 c.rhodecode_db_repo = db_repo
568 566 c.rhodecode_repo = self.rhodecode_repo
569 567 c.repository_requirements_missing = missing_requirements
570 568
571 569 self._update_global_counters(self.scm_model, db_repo)
572 570
573 571 def _update_global_counters(self, scm_model, db_repo):
574 572 """
575 573 Base variables that are exposed to every page of repository
576 574 """
577 575 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
578 576
579 577 def _handle_missing_requirements(self, error):
580 578 self.rhodecode_repo = None
581 579 log.error(
582 580 'Requirements are missing for repository %s: %s',
583 581 c.repo_name, error.message)
584 582
585 583 summary_url = url('summary_home', repo_name=c.repo_name)
586 584 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
587 585 settings_update_url = url('repo', repo_name=c.repo_name)
588 586 path = request.path
589 587 should_redirect = (
590 588 path not in (summary_url, settings_update_url)
591 589 and '/settings' not in path or path == statistics_url
592 590 )
593 591 if should_redirect:
594 592 redirect(summary_url)
@@ -1,312 +1,312 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import io
21 21 import re
22 22 import datetime
23 23 import logging
24 24 import pylons
25 25 import Queue
26 26 import subprocess32
27 27 import os
28 28
29 29 from pyramid.i18n import get_localizer
30 30 from pyramid.threadlocal import get_current_request
31 31 from pyramid.interfaces import IRoutesMapper
32 32 from pyramid.settings import asbool
33 33 from pyramid.path import AssetResolver
34 34 from threading import Thread
35 35
36 36 from rhodecode.translation import _ as tsf
37 37 from rhodecode.config.jsroutes import generate_jsroutes_content
38 38
39 39 import rhodecode
40 40
41 41 from pylons.i18n.translation import _get_translator
42 42 from pylons.util import ContextObj
43 43 from routes.util import URLGenerator
44 44
45 45 from rhodecode.lib.base import attach_context_attributes, get_auth_user
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 def add_renderer_globals(event):
51 51 # Put pylons stuff into the context. This will be removed as soon as
52 52 # migration to pyramid is finished.
53 53 conf = pylons.config._current_obj()
54 54 event['h'] = conf.get('pylons.h')
55 55 event['c'] = pylons.tmpl_context
56 56 event['url'] = pylons.url
57 57
58 58 # TODO: When executed in pyramid view context the request is not available
59 59 # in the event. Find a better solution to get the request.
60 60 request = event['request'] or get_current_request()
61 61
62 62 # Add Pyramid translation as '_' to context
63 63 event['_'] = request.translate
64 64 event['_ungettext'] = request.plularize
65 65
66 66
67 67 def add_localizer(event):
68 68 request = event.request
69 69 localizer = get_localizer(request)
70 70
71 71 def auto_translate(*args, **kwargs):
72 72 return localizer.translate(tsf(*args, **kwargs))
73 73
74 74 request.localizer = localizer
75 75 request.translate = auto_translate
76 76 request.plularize = localizer.pluralize
77 77
78 78
79 79 def set_user_lang(event):
80 80 request = event.request
81 81 cur_user = getattr(request, 'user', None)
82 82
83 83 if cur_user:
84 84 user_lang = cur_user.get_instance().user_data.get('language')
85 85 if user_lang:
86 86 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
87 87 event.request._LOCALE_ = user_lang
88 88
89 89
90 90 def add_pylons_context(event):
91 91 request = event.request
92 92
93 93 config = rhodecode.CONFIG
94 94 environ = request.environ
95 95 session = request.session
96 96
97 97 if hasattr(request, 'vcs_call'):
98 98 # skip vcs calls
99 99 return
100 100
101 101 # Setup pylons globals.
102 102 pylons.config._push_object(config)
103 103 pylons.request._push_object(request)
104 104 pylons.session._push_object(session)
105 105 pylons.translator._push_object(_get_translator(config.get('lang')))
106 106
107 107 pylons.url._push_object(URLGenerator(config['routes.map'], environ))
108 108 session_key = (
109 109 config['pylons.environ_config'].get('session', 'beaker.session'))
110 110 environ[session_key] = session
111 111
112 112 if hasattr(request, 'rpc_method'):
113 113 # skip api calls
114 114 return
115 115
116 116 # Get the rhodecode auth user object and make it available.
117 117 auth_user = get_auth_user(environ)
118 118 request.user = auth_user
119 119 environ['rc_auth_user'] = auth_user
120 120
121 121 # Setup the pylons context object ('c')
122 122 context = ContextObj()
123 123 context.rhodecode_user = auth_user
124 attach_context_attributes(context, request)
124 attach_context_attributes(context, request, request.user.user_id)
125 125 pylons.tmpl_context._push_object(context)
126 126
127 127
128 128 def scan_repositories_if_enabled(event):
129 129 """
130 130 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
131 131 does a repository scan if enabled in the settings.
132 132 """
133 133 settings = event.app.registry.settings
134 134 vcs_server_enabled = settings['vcs.server.enable']
135 135 import_on_startup = settings['startup.import_repos']
136 136 if vcs_server_enabled and import_on_startup:
137 137 from rhodecode.model.scm import ScmModel
138 138 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
139 139 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
140 140 repo2db_mapper(repositories, remove_obsolete=False)
141 141
142 142
143 143 def write_metadata_if_needed(event):
144 144 """
145 145 Writes upgrade metadata
146 146 """
147 147 import rhodecode
148 148 from rhodecode.lib import system_info
149 149 from rhodecode.lib import ext_json
150 150
151 151 def write():
152 152 fname = '.rcmetadata.json'
153 153 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
154 154 metadata_destination = os.path.join(ini_loc, fname)
155 155
156 156 configuration = system_info.SysInfo(
157 157 system_info.rhodecode_config)()['value']
158 158 license_token = configuration['config']['license_token']
159 159 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
160 160 del dbinfo['url']
161 161 metadata = dict(
162 162 desc='upgrade metadata info',
163 163 license_token=license_token,
164 164 created_on=datetime.datetime.utcnow().isoformat(),
165 165 usage=system_info.SysInfo(system_info.usage_info)()['value'],
166 166 platform=system_info.SysInfo(system_info.platform_type)()['value'],
167 167 database=dbinfo,
168 168 cpu=system_info.SysInfo(system_info.cpu)()['value'],
169 169 memory=system_info.SysInfo(system_info.memory)()['value'],
170 170 )
171 171
172 172 with open(metadata_destination, 'wb') as f:
173 173 f.write(ext_json.json.dumps(metadata))
174 174
175 175 settings = event.app.registry.settings
176 176 if settings.get('metadata.skip'):
177 177 return
178 178
179 179 try:
180 180 write()
181 181 except Exception:
182 182 pass
183 183
184 184
185 185 def write_js_routes_if_enabled(event):
186 186 registry = event.app.registry
187 187
188 188 mapper = registry.queryUtility(IRoutesMapper)
189 189 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
190 190
191 191 def _extract_route_information(route):
192 192 """
193 193 Convert a route into tuple(name, path, args), eg:
194 194 ('show_user', '/profile/%(username)s', ['username'])
195 195 """
196 196
197 197 routepath = route.pattern
198 198 pattern = route.pattern
199 199
200 200 def replace(matchobj):
201 201 if matchobj.group(1):
202 202 return "%%(%s)s" % matchobj.group(1).split(':')[0]
203 203 else:
204 204 return "%%(%s)s" % matchobj.group(2)
205 205
206 206 routepath = _argument_prog.sub(replace, routepath)
207 207
208 208 return (
209 209 route.name,
210 210 routepath,
211 211 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
212 212 for arg in _argument_prog.findall(pattern)]
213 213 )
214 214
215 215 def get_routes():
216 216 # pylons routes
217 217 for route in rhodecode.CONFIG['routes.map'].jsroutes():
218 218 yield route
219 219
220 220 # pyramid routes
221 221 for route in mapper.get_routes():
222 222 if not route.name.startswith('__'):
223 223 yield _extract_route_information(route)
224 224
225 225 if asbool(registry.settings.get('generate_js_files', 'false')):
226 226 static_path = AssetResolver().resolve('rhodecode:public').abspath()
227 227 jsroutes = get_routes()
228 228 jsroutes_file_content = generate_jsroutes_content(jsroutes)
229 229 jsroutes_file_path = os.path.join(
230 230 static_path, 'js', 'rhodecode', 'routes.js')
231 231
232 232 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
233 233 f.write(jsroutes_file_content)
234 234
235 235
236 236 class Subscriber(object):
237 237 """
238 238 Base class for subscribers to the pyramid event system.
239 239 """
240 240 def __call__(self, event):
241 241 self.run(event)
242 242
243 243 def run(self, event):
244 244 raise NotImplementedError('Subclass has to implement this.')
245 245
246 246
247 247 class AsyncSubscriber(Subscriber):
248 248 """
249 249 Subscriber that handles the execution of events in a separate task to not
250 250 block the execution of the code which triggers the event. It puts the
251 251 received events into a queue from which the worker process takes them in
252 252 order.
253 253 """
254 254 def __init__(self):
255 255 self._stop = False
256 256 self._eventq = Queue.Queue()
257 257 self._worker = self.create_worker()
258 258 self._worker.start()
259 259
260 260 def __call__(self, event):
261 261 self._eventq.put(event)
262 262
263 263 def create_worker(self):
264 264 worker = Thread(target=self.do_work)
265 265 worker.daemon = True
266 266 return worker
267 267
268 268 def stop_worker(self):
269 269 self._stop = False
270 270 self._eventq.put(None)
271 271 self._worker.join()
272 272
273 273 def do_work(self):
274 274 while not self._stop:
275 275 event = self._eventq.get()
276 276 if event is not None:
277 277 self.run(event)
278 278
279 279
280 280 class AsyncSubprocessSubscriber(AsyncSubscriber):
281 281 """
282 282 Subscriber that uses the subprocess32 module to execute a command if an
283 283 event is received. Events are handled asynchronously.
284 284 """
285 285
286 286 def __init__(self, cmd, timeout=None):
287 287 super(AsyncSubprocessSubscriber, self).__init__()
288 288 self._cmd = cmd
289 289 self._timeout = timeout
290 290
291 291 def run(self, event):
292 292 cmd = self._cmd
293 293 timeout = self._timeout
294 294 log.debug('Executing command %s.', cmd)
295 295
296 296 try:
297 297 output = subprocess32.check_output(
298 298 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
299 299 log.debug('Command finished %s', cmd)
300 300 if output:
301 301 log.debug('Command output: %s', output)
302 302 except subprocess32.TimeoutExpired as e:
303 303 log.exception('Timeout while executing command.')
304 304 if e.output:
305 305 log.error('Command output: %s', e.output)
306 306 except subprocess32.CalledProcessError as e:
307 307 log.exception('Error while executing command.')
308 308 if e.output:
309 309 log.error('Command output: %s', e.output)
310 310 except:
311 311 log.exception(
312 312 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now