##// END OF EJS Templates
pyramid: moved extraction of user into a seperate subscriber.
marcink -
r1903:982ccdbd default
parent child Browse files
Show More
@@ -1,604 +1,607 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 268 def calculate_version_hash():
269 269 return md5(
270 270 config.get('beaker.session.secret', '') +
271 271 rhodecode.__version__)[:8]
272 272
273 273
274 274 def attach_context_attributes(context, request, user_id):
275 275 """
276 276 Attach variables into template context called `c`, please note that
277 277 request could be pylons or pyramid request in here.
278 278 """
279 279 rc_config = SettingsModel().get_all_settings(cache=True)
280 280
281 281 context.rhodecode_version = rhodecode.__version__
282 282 context.rhodecode_edition = config.get('rhodecode.edition')
283 283 # unique secret + version does not leak the version but keep consistency
284 284 context.rhodecode_version_hash = calculate_version_hash()
285 285
286 286 # Default language set for the incoming request
287 287 context.language = translation.get_lang()[0]
288 288
289 289 # Visual options
290 290 context.visual = AttributeDict({})
291 291
292 292 # DB stored Visual Items
293 293 context.visual.show_public_icon = str2bool(
294 294 rc_config.get('rhodecode_show_public_icon'))
295 295 context.visual.show_private_icon = str2bool(
296 296 rc_config.get('rhodecode_show_private_icon'))
297 297 context.visual.stylify_metatags = str2bool(
298 298 rc_config.get('rhodecode_stylify_metatags'))
299 299 context.visual.dashboard_items = safe_int(
300 300 rc_config.get('rhodecode_dashboard_items', 100))
301 301 context.visual.admin_grid_items = safe_int(
302 302 rc_config.get('rhodecode_admin_grid_items', 100))
303 303 context.visual.repository_fields = str2bool(
304 304 rc_config.get('rhodecode_repository_fields'))
305 305 context.visual.show_version = str2bool(
306 306 rc_config.get('rhodecode_show_version'))
307 307 context.visual.use_gravatar = str2bool(
308 308 rc_config.get('rhodecode_use_gravatar'))
309 309 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
310 310 context.visual.default_renderer = rc_config.get(
311 311 'rhodecode_markup_renderer', 'rst')
312 312 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
313 313 context.visual.rhodecode_support_url = \
314 314 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
315 315
316 316 context.pre_code = rc_config.get('rhodecode_pre_code')
317 317 context.post_code = rc_config.get('rhodecode_post_code')
318 318 context.rhodecode_name = rc_config.get('rhodecode_title')
319 319 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
320 320 # if we have specified default_encoding in the request, it has more
321 321 # priority
322 322 if request.GET.get('default_encoding'):
323 323 context.default_encodings.insert(0, request.GET.get('default_encoding'))
324 324 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
325 325
326 326 # INI stored
327 327 context.labs_active = str2bool(
328 328 config.get('labs_settings_active', 'false'))
329 329 context.visual.allow_repo_location_change = str2bool(
330 330 config.get('allow_repo_location_change', True))
331 331 context.visual.allow_custom_hooks_settings = str2bool(
332 332 config.get('allow_custom_hooks_settings', True))
333 333 context.debug_style = str2bool(config.get('debug_style', False))
334 334
335 335 context.rhodecode_instanceid = config.get('instance_id')
336 336
337 337 context.visual.cut_off_limit_diff = safe_int(
338 338 config.get('cut_off_limit_diff'))
339 339 context.visual.cut_off_limit_file = safe_int(
340 340 config.get('cut_off_limit_file'))
341 341
342 342 # AppEnlight
343 343 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
344 344 context.appenlight_api_public_key = config.get(
345 345 'appenlight.api_public_key', '')
346 346 context.appenlight_server_url = config.get('appenlight.server_url', '')
347 347
348 348 # JS template context
349 349 context.template_context = {
350 350 'repo_name': None,
351 351 'repo_type': None,
352 352 'repo_landing_commit': None,
353 353 'rhodecode_user': {
354 354 'username': None,
355 355 'email': None,
356 356 'notification_status': False
357 357 },
358 358 'visual': {
359 359 'default_renderer': None
360 360 },
361 361 'commit_data': {
362 362 'commit_id': None
363 363 },
364 364 'pull_request_data': {'pull_request_id': None},
365 365 'timeago': {
366 366 'refresh_time': 120 * 1000,
367 367 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
368 368 },
369 369 'pylons_dispatch': {
370 370 # 'controller': request.environ['pylons.routes_dict']['controller'],
371 371 # 'action': request.environ['pylons.routes_dict']['action'],
372 372 },
373 373 'pyramid_dispatch': {
374 374
375 375 },
376 376 'extra': {'plugins': {}}
377 377 }
378 378 # END CONFIG VARS
379 379
380 380 # TODO: This dosn't work when called from pylons compatibility tween.
381 381 # Fix this and remove it from base controller.
382 382 # context.repo_name = get_repo_slug(request) # can be empty
383 383
384 384 diffmode = 'sideside'
385 385 if request.GET.get('diffmode'):
386 386 if request.GET['diffmode'] == 'unified':
387 387 diffmode = 'unified'
388 388 elif request.session.get('diffmode'):
389 389 diffmode = request.session['diffmode']
390 390
391 391 context.diffmode = diffmode
392 392
393 393 if request.session.get('diffmode') != diffmode:
394 394 request.session['diffmode'] = diffmode
395 395
396 396 context.csrf_token = auth.get_csrf_token()
397 397 context.backends = rhodecode.BACKENDS.keys()
398 398 context.backends.sort()
399 399 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
400 400 context.pyramid_request = pyramid.threadlocal.get_current_request()
401 401
402 402 # attach the whole call context to the request
403 403 request.call_context = context
404 404
405 405
406 def get_auth_user(environ):
406 def get_auth_user(request):
407 environ = request.environ
408 session = request.session
409
407 410 ip_addr = get_ip_addr(environ)
408 411 # make sure that we update permissions each time we call controller
409 412 _auth_token = (request.GET.get('auth_token', '') or
410 413 request.GET.get('api_key', ''))
411 414
412 415 if _auth_token:
413 416 # when using API_KEY we assume user exists, and
414 417 # doesn't need auth based on cookies.
415 418 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
416 419 authenticated = False
417 420 else:
418 421 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
419 422 try:
420 423 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
421 424 ip_addr=ip_addr)
422 425 except UserCreationError as e:
423 426 h.flash(e, 'error')
424 427 # container auth or other auth functions that create users
425 428 # on the fly can throw this exception signaling that there's
426 429 # issue with user creation, explanation should be provided
427 430 # in Exception itself. We then create a simple blank
428 431 # AuthUser
429 432 auth_user = AuthUser(ip_addr=ip_addr)
430 433
431 434 if password_changed(auth_user, session):
432 435 session.invalidate()
433 436 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
434 437 auth_user = AuthUser(ip_addr=ip_addr)
435 438
436 439 authenticated = cookie_store.get('is_authenticated')
437 440
438 441 if not auth_user.is_authenticated and auth_user.is_user_object:
439 442 # user is not authenticated and not empty
440 443 auth_user.set_authenticated(authenticated)
441 444
442 445 return auth_user
443 446
444 447
445 448 class BaseController(WSGIController):
446 449
447 450 def __before__(self):
448 451 """
449 452 __before__ is called before controller methods and after __call__
450 453 """
451 454 # on each call propagate settings calls into global settings.
452 455 set_rhodecode_config(config)
453 456 attach_context_attributes(c, request, c.rhodecode_user.user_id)
454 457
455 458 # TODO: Remove this when fixed in attach_context_attributes()
456 459 c.repo_name = get_repo_slug(request) # can be empty
457 460
458 461 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
459 462 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
460 463 self.sa = meta.Session
461 464 self.scm_model = ScmModel(self.sa)
462 465
463 466 # set user language
464 467 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
465 468 if user_lang:
466 469 translation.set_lang(user_lang)
467 470 log.debug('set language to %s for user %s',
468 471 user_lang, self._rhodecode_user)
469 472
470 473 def _dispatch_redirect(self, with_url, environ, start_response):
471 474 resp = HTTPFound(with_url)
472 475 environ['SCRIPT_NAME'] = '' # handle prefix middleware
473 476 environ['PATH_INFO'] = with_url
474 477 return resp(environ, start_response)
475 478
476 479 def __call__(self, environ, start_response):
477 480 """Invoke the Controller"""
478 481 # WSGIController.__call__ dispatches to the Controller method
479 482 # the request is routed to. This routing information is
480 483 # available in environ['pylons.routes_dict']
481 484 from rhodecode.lib import helpers as h
482 485
483 486 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
484 487 if environ.get('debugtoolbar.wants_pylons_context', False):
485 488 environ['debugtoolbar.pylons_context'] = c._current_obj()
486 489
487 490 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
488 491 environ['pylons.routes_dict']['action']])
489 492
490 493 self.rc_config = SettingsModel().get_all_settings(cache=True)
491 494 self.ip_addr = get_ip_addr(environ)
492 495
493 496 # The rhodecode auth user is looked up and passed through the
494 497 # environ by the pylons compatibility tween in pyramid.
495 498 # So we can just grab it from there.
496 499 auth_user = environ['rc_auth_user']
497 500
498 501 # set globals for auth user
499 502 request.user = auth_user
500 503 c.rhodecode_user = self._rhodecode_user = auth_user
501 504
502 505 log.info('IP: %s User: %s accessed %s [%s]' % (
503 506 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
504 507 _route_name)
505 508 )
506 509
507 510 user_obj = auth_user.get_instance()
508 511 if user_obj and user_obj.user_data.get('force_password_change'):
509 512 h.flash('You are required to change your password', 'warning',
510 513 ignore_duplicate=True)
511 514 return self._dispatch_redirect(
512 515 url('my_account_password'), environ, start_response)
513 516
514 517 return WSGIController.__call__(self, environ, start_response)
515 518
516 519
517 520 class BaseRepoController(BaseController):
518 521 """
519 522 Base class for controllers responsible for loading all needed data for
520 523 repository loaded items are
521 524
522 525 c.rhodecode_repo: instance of scm repository
523 526 c.rhodecode_db_repo: instance of db
524 527 c.repository_requirements_missing: shows that repository specific data
525 528 could not be displayed due to the missing requirements
526 529 c.repository_pull_requests: show number of open pull requests
527 530 """
528 531
529 532 def __before__(self):
530 533 super(BaseRepoController, self).__before__()
531 534 if c.repo_name: # extracted from routes
532 535 db_repo = Repository.get_by_repo_name(c.repo_name)
533 536 if not db_repo:
534 537 return
535 538
536 539 log.debug(
537 540 'Found repository in database %s with state `%s`',
538 541 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
539 542 route = getattr(request.environ.get('routes.route'), 'name', '')
540 543
541 544 # allow to delete repos that are somehow damages in filesystem
542 545 if route in ['delete_repo']:
543 546 return
544 547
545 548 if db_repo.repo_state in [Repository.STATE_PENDING]:
546 549 if route in ['repo_creating_home']:
547 550 return
548 551 check_url = url('repo_creating_home', repo_name=c.repo_name)
549 552 return redirect(check_url)
550 553
551 554 self.rhodecode_db_repo = db_repo
552 555
553 556 missing_requirements = False
554 557 try:
555 558 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
556 559 except RepositoryRequirementError as e:
557 560 missing_requirements = True
558 561 self._handle_missing_requirements(e)
559 562
560 563 if self.rhodecode_repo is None and not missing_requirements:
561 564 log.error('%s this repository is present in database but it '
562 565 'cannot be created as an scm instance', c.repo_name)
563 566
564 567 h.flash(_(
565 568 "The repository at %(repo_name)s cannot be located.") %
566 569 {'repo_name': c.repo_name},
567 570 category='error', ignore_duplicate=True)
568 571 redirect(h.route_path('home'))
569 572
570 573 # update last change according to VCS data
571 574 if not missing_requirements:
572 575 commit = db_repo.get_commit(
573 576 pre_load=["author", "date", "message", "parents"])
574 577 db_repo.update_commit_cache(commit)
575 578
576 579 # Prepare context
577 580 c.rhodecode_db_repo = db_repo
578 581 c.rhodecode_repo = self.rhodecode_repo
579 582 c.repository_requirements_missing = missing_requirements
580 583
581 584 self._update_global_counters(self.scm_model, db_repo)
582 585
583 586 def _update_global_counters(self, scm_model, db_repo):
584 587 """
585 588 Base variables that are exposed to every page of repository
586 589 """
587 590 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
588 591
589 592 def _handle_missing_requirements(self, error):
590 593 self.rhodecode_repo = None
591 594 log.error(
592 595 'Requirements are missing for repository %s: %s',
593 596 c.repo_name, error.message)
594 597
595 598 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
596 599 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
597 600 settings_update_url = url('repo', repo_name=c.repo_name)
598 601 path = request.path
599 602 should_redirect = (
600 603 path not in (summary_url, settings_update_url)
601 604 and '/settings' not in path or path == statistics_url
602 605 )
603 606 if should_redirect:
604 607 redirect(summary_url)
@@ -1,315 +1,329 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 def add_request_user_context(event):
91 """
92 Adds auth user into request context
93 """
94 request = event.request
95
96 if hasattr(request, 'vcs_call'):
97 # skip vcs calls
98 return
99
100 if hasattr(request, 'rpc_method'):
101 # skip api calls
102 return
103
104 auth_user = get_auth_user(request)
105 request.user = auth_user
106 request.environ['rc_auth_user'] = auth_user
107
108
90 109 def add_pylons_context(event):
91 110 request = event.request
92 111
93 112 config = rhodecode.CONFIG
94 113 environ = request.environ
95 114 session = request.session
96 115
97 116 if hasattr(request, 'vcs_call'):
98 117 # skip vcs calls
99 118 return
100 119
101 120 # Setup pylons globals.
102 121 pylons.config._push_object(config)
103 122 pylons.request._push_object(request)
104 123 pylons.session._push_object(session)
105 124 pylons.translator._push_object(_get_translator(config.get('lang')))
106 125
107 126 pylons.url._push_object(URLGenerator(config['routes.map'], environ))
108 127 session_key = (
109 128 config['pylons.environ_config'].get('session', 'beaker.session'))
110 129 environ[session_key] = session
111 130
112 131 if hasattr(request, 'rpc_method'):
113 132 # skip api calls
114 133 return
115 134
116 # Get the rhodecode auth user object and make it available.
117 auth_user = get_auth_user(environ)
118 request.user = auth_user
119 environ['rc_auth_user'] = auth_user
120
121 135 # Setup the pylons context object ('c')
122 136 context = ContextObj()
123 context.rhodecode_user = auth_user
137 context.rhodecode_user = request.user
124 138 attach_context_attributes(context, request, request.user.user_id)
125 139 pylons.tmpl_context._push_object(context)
126 140
127 141
128 142 def scan_repositories_if_enabled(event):
129 143 """
130 144 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
131 145 does a repository scan if enabled in the settings.
132 146 """
133 147 settings = event.app.registry.settings
134 148 vcs_server_enabled = settings['vcs.server.enable']
135 149 import_on_startup = settings['startup.import_repos']
136 150 if vcs_server_enabled and import_on_startup:
137 151 from rhodecode.model.scm import ScmModel
138 152 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
139 153 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
140 154 repo2db_mapper(repositories, remove_obsolete=False)
141 155
142 156
143 157 def write_metadata_if_needed(event):
144 158 """
145 159 Writes upgrade metadata
146 160 """
147 161 import rhodecode
148 162 from rhodecode.lib import system_info
149 163 from rhodecode.lib import ext_json
150 164
151 165 def write():
152 166 fname = '.rcmetadata.json'
153 167 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
154 168 metadata_destination = os.path.join(ini_loc, fname)
155 169
156 170 configuration = system_info.SysInfo(
157 171 system_info.rhodecode_config)()['value']
158 172 license_token = configuration['config']['license_token']
159 173 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
160 174 del dbinfo['url']
161 175 metadata = dict(
162 176 desc='upgrade metadata info',
163 177 license_token=license_token,
164 178 created_on=datetime.datetime.utcnow().isoformat(),
165 179 usage=system_info.SysInfo(system_info.usage_info)()['value'],
166 180 platform=system_info.SysInfo(system_info.platform_type)()['value'],
167 181 database=dbinfo,
168 182 cpu=system_info.SysInfo(system_info.cpu)()['value'],
169 183 memory=system_info.SysInfo(system_info.memory)()['value'],
170 184 )
171 185
172 186 with open(metadata_destination, 'wb') as f:
173 187 f.write(ext_json.json.dumps(metadata))
174 188
175 189 settings = event.app.registry.settings
176 190 if settings.get('metadata.skip'):
177 191 return
178 192
179 193 try:
180 194 write()
181 195 except Exception:
182 196 pass
183 197
184 198
185 199 def write_js_routes_if_enabled(event):
186 200 registry = event.app.registry
187 201
188 202 mapper = registry.queryUtility(IRoutesMapper)
189 203 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
190 204
191 205 def _extract_route_information(route):
192 206 """
193 207 Convert a route into tuple(name, path, args), eg:
194 208 ('show_user', '/profile/%(username)s', ['username'])
195 209 """
196 210
197 211 routepath = route.pattern
198 212 pattern = route.pattern
199 213
200 214 def replace(matchobj):
201 215 if matchobj.group(1):
202 216 return "%%(%s)s" % matchobj.group(1).split(':')[0]
203 217 else:
204 218 return "%%(%s)s" % matchobj.group(2)
205 219
206 220 routepath = _argument_prog.sub(replace, routepath)
207 221
208 222 if not routepath.startswith('/'):
209 223 routepath = '/'+routepath
210 224
211 225 return (
212 226 route.name,
213 227 routepath,
214 228 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
215 229 for arg in _argument_prog.findall(pattern)]
216 230 )
217 231
218 232 def get_routes():
219 233 # pylons routes
220 234 for route in rhodecode.CONFIG['routes.map'].jsroutes():
221 235 yield route
222 236
223 237 # pyramid routes
224 238 for route in mapper.get_routes():
225 239 if not route.name.startswith('__'):
226 240 yield _extract_route_information(route)
227 241
228 242 if asbool(registry.settings.get('generate_js_files', 'false')):
229 243 static_path = AssetResolver().resolve('rhodecode:public').abspath()
230 244 jsroutes = get_routes()
231 245 jsroutes_file_content = generate_jsroutes_content(jsroutes)
232 246 jsroutes_file_path = os.path.join(
233 247 static_path, 'js', 'rhodecode', 'routes.js')
234 248
235 249 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
236 250 f.write(jsroutes_file_content)
237 251
238 252
239 253 class Subscriber(object):
240 254 """
241 255 Base class for subscribers to the pyramid event system.
242 256 """
243 257 def __call__(self, event):
244 258 self.run(event)
245 259
246 260 def run(self, event):
247 261 raise NotImplementedError('Subclass has to implement this.')
248 262
249 263
250 264 class AsyncSubscriber(Subscriber):
251 265 """
252 266 Subscriber that handles the execution of events in a separate task to not
253 267 block the execution of the code which triggers the event. It puts the
254 268 received events into a queue from which the worker process takes them in
255 269 order.
256 270 """
257 271 def __init__(self):
258 272 self._stop = False
259 273 self._eventq = Queue.Queue()
260 274 self._worker = self.create_worker()
261 275 self._worker.start()
262 276
263 277 def __call__(self, event):
264 278 self._eventq.put(event)
265 279
266 280 def create_worker(self):
267 281 worker = Thread(target=self.do_work)
268 282 worker.daemon = True
269 283 return worker
270 284
271 285 def stop_worker(self):
272 286 self._stop = False
273 287 self._eventq.put(None)
274 288 self._worker.join()
275 289
276 290 def do_work(self):
277 291 while not self._stop:
278 292 event = self._eventq.get()
279 293 if event is not None:
280 294 self.run(event)
281 295
282 296
283 297 class AsyncSubprocessSubscriber(AsyncSubscriber):
284 298 """
285 299 Subscriber that uses the subprocess32 module to execute a command if an
286 300 event is received. Events are handled asynchronously.
287 301 """
288 302
289 303 def __init__(self, cmd, timeout=None):
290 304 super(AsyncSubprocessSubscriber, self).__init__()
291 305 self._cmd = cmd
292 306 self._timeout = timeout
293 307
294 308 def run(self, event):
295 309 cmd = self._cmd
296 310 timeout = self._timeout
297 311 log.debug('Executing command %s.', cmd)
298 312
299 313 try:
300 314 output = subprocess32.check_output(
301 315 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
302 316 log.debug('Command finished %s', cmd)
303 317 if output:
304 318 log.debug('Command output: %s', output)
305 319 except subprocess32.TimeoutExpired as e:
306 320 log.exception('Timeout while executing command.')
307 321 if e.output:
308 322 log.error('Command output: %s', e.output)
309 323 except subprocess32.CalledProcessError as e:
310 324 log.exception('Error while executing command.')
311 325 if e.output:
312 326 log.error('Command output: %s', e.output)
313 327 except:
314 328 log.exception(
315 329 'Exception while executing command %s.', cmd)
@@ -1,66 +1,68 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 import logging
23 23
24 24 from rhodecode.lib.middleware.vcs import (
25 25 detect_vcs_request, VCS_TYPE_KEY, VCS_TYPE_SKIP)
26 26
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def vcs_detection_tween_factory(handler, registry):
32 32
33 33 def vcs_detection_tween(request):
34 34 """
35 35 Do detection of vcs type, and save results for other layers to re-use
36 36 this information
37 37 """
38 38
39 39 vcs_handler = detect_vcs_request(
40 40 request.environ, request.registry.settings.get('vcs.backends'))
41 41
42 42 if vcs_handler:
43 43 # save detected VCS type for later re-use
44 44 request.environ[VCS_TYPE_KEY] = vcs_handler.SCM
45 45 request.vcs_call = vcs_handler.SCM
46 46 return handler(request)
47 47
48 48 # mark that we didn't detect an VCS, and we can skip detection later on
49 49 request.environ[VCS_TYPE_KEY] = VCS_TYPE_SKIP
50 50
51 51 return handler(request)
52 52
53 53 return vcs_detection_tween
54 54
55 55
56 56 def includeme(config):
57 57 config.add_subscriber('rhodecode.subscribers.add_renderer_globals',
58 58 'pyramid.events.BeforeRender')
59 59 config.add_subscriber('rhodecode.subscribers.set_user_lang',
60 60 'pyramid.events.NewRequest')
61 61 config.add_subscriber('rhodecode.subscribers.add_localizer',
62 62 'pyramid.events.NewRequest')
63 config.add_subscriber('rhodecode.subscribers.add_request_user_context',
64 'pyramid.events.ContextFound')
63 65 config.add_subscriber('rhodecode.subscribers.add_pylons_context',
64 66 'pyramid.events.ContextFound')
65 67
66 68 config.add_tween('rhodecode.tweens.vcs_detection_tween_factory')
General Comments 0
You need to be logged in to leave comments. Login now