##// END OF EJS Templates
base: add pyramid_request to tmplContext
ergo -
r765:f8f4537e default
parent child Browse files
Show More
@@ -1,587 +1,590 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 import pyramid.threadlocal
31 32
32 33 from paste.auth.basic import AuthBasicAuthenticator
33 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 36 from pylons import config, tmpl_context as c, request, session, url
36 37 from pylons.controllers import WSGIController
37 38 from pylons.controllers.util import redirect
38 39 from pylons.i18n import translation
39 40 # marcink: don't remove this import
40 41 from pylons.templating import render_mako as render # noqa
41 42 from pylons.i18n.translation import _
42 43 from webob.exc import HTTPFound
43 44
44 45
45 46 import rhodecode
46 47 from rhodecode.authentication.base import VCS_TYPE
47 48 from rhodecode.lib import auth, utils2
48 49 from rhodecode.lib import helpers as h
49 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 51 from rhodecode.lib.exceptions import UserCreationError
51 52 from rhodecode.lib.utils import (
52 53 get_repo_slug, set_rhodecode_config, password_changed,
53 54 get_enabled_hook_classes)
54 55 from rhodecode.lib.utils2 import (
55 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 58 from rhodecode.model import meta
58 59 from rhodecode.model.db import Repository, User
59 60 from rhodecode.model.notification import NotificationModel
60 61 from rhodecode.model.scm import ScmModel
61 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 63
63 64
64 65 log = logging.getLogger(__name__)
65 66
66 67
67 68 def _filter_proxy(ip):
68 69 """
69 70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 71 ips. Those comma separated IPs are passed from various proxies in the
71 72 chain of request processing. The left-most being the original client.
72 73 We only care about the first IP which came from the org. client.
73 74
74 75 :param ip: ip string from headers
75 76 """
76 77 if ',' in ip:
77 78 _ips = ip.split(',')
78 79 _first_ip = _ips[0].strip()
79 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 81 return _first_ip
81 82 return ip
82 83
83 84
84 85 def _filter_port(ip):
85 86 """
86 87 Removes a port from ip, there are 4 main cases to handle here.
87 88 - ipv4 eg. 127.0.0.1
88 89 - ipv6 eg. ::1
89 90 - ipv4+port eg. 127.0.0.1:8080
90 91 - ipv6+port eg. [::1]:8080
91 92
92 93 :param ip:
93 94 """
94 95 def is_ipv6(ip_addr):
95 96 if hasattr(socket, 'inet_pton'):
96 97 try:
97 98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 99 except socket.error:
99 100 return False
100 101 else:
101 102 # fallback to ipaddress
102 103 try:
103 104 ipaddress.IPv6Address(ip_addr)
104 105 except Exception:
105 106 return False
106 107 return True
107 108
108 109 if ':' not in ip: # must be ipv4 pure ip
109 110 return ip
110 111
111 112 if '[' in ip and ']' in ip: # ipv6 with port
112 113 return ip.split(']')[0][1:].lower()
113 114
114 115 # must be ipv6 or ipv4 with port
115 116 if is_ipv6(ip):
116 117 return ip
117 118 else:
118 119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 120 return ip
120 121
121 122
122 123 def get_ip_addr(environ):
123 124 proxy_key = 'HTTP_X_REAL_IP'
124 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 126 def_key = 'REMOTE_ADDR'
126 127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 128
128 129 ip = environ.get(proxy_key)
129 130 if ip:
130 131 return _filters(ip)
131 132
132 133 ip = environ.get(proxy_key2)
133 134 if ip:
134 135 return _filters(ip)
135 136
136 137 ip = environ.get(def_key, '0.0.0.0')
137 138 return _filters(ip)
138 139
139 140
140 141 def get_server_ip_addr(environ, log_errors=True):
141 142 hostname = environ.get('SERVER_NAME')
142 143 try:
143 144 return socket.gethostbyname(hostname)
144 145 except Exception as e:
145 146 if log_errors:
146 147 # in some cases this lookup is not possible, and we don't want to
147 148 # make it an exception in logs
148 149 log.exception('Could not retrieve server ip address: %s', e)
149 150 return hostname
150 151
151 152
152 153 def get_server_port(environ):
153 154 return environ.get('SERVER_PORT')
154 155
155 156
156 157 def get_access_path(environ):
157 158 path = environ.get('PATH_INFO')
158 159 org_req = environ.get('pylons.original_request')
159 160 if org_req:
160 161 path = org_req.environ.get('PATH_INFO')
161 162 return path
162 163
163 164
164 165 def vcs_operation_context(
165 166 environ, repo_name, username, action, scm, check_locking=True):
166 167 """
167 168 Generate the context for a vcs operation, e.g. push or pull.
168 169
169 170 This context is passed over the layers so that hooks triggered by the
170 171 vcs operation know details like the user, the user's IP address etc.
171 172
172 173 :param check_locking: Allows to switch of the computation of the locking
173 174 data. This serves mainly the need of the simplevcs middleware to be
174 175 able to disable this for certain operations.
175 176
176 177 """
177 178 # Tri-state value: False: unlock, None: nothing, True: lock
178 179 make_lock = None
179 180 locked_by = [None, None, None]
180 181 is_anonymous = username == User.DEFAULT_USER
181 182 if not is_anonymous and check_locking:
182 183 log.debug('Checking locking on repository "%s"', repo_name)
183 184 user = User.get_by_username(username)
184 185 repo = Repository.get_by_repo_name(repo_name)
185 186 make_lock, __, locked_by = repo.get_locking_state(
186 187 action, user.user_id)
187 188
188 189 settings_model = VcsSettingsModel(repo=repo_name)
189 190 ui_settings = settings_model.get_ui_settings()
190 191
191 192 extras = {
192 193 'ip': get_ip_addr(environ),
193 194 'username': username,
194 195 'action': action,
195 196 'repository': repo_name,
196 197 'scm': scm,
197 198 'config': rhodecode.CONFIG['__file__'],
198 199 'make_lock': make_lock,
199 200 'locked_by': locked_by,
200 201 'server_url': utils2.get_server_url(environ),
201 202 'hooks': get_enabled_hook_classes(ui_settings),
202 203 }
203 204 return extras
204 205
205 206
206 207 class BasicAuth(AuthBasicAuthenticator):
207 208
208 209 def __init__(self, realm, authfunc, registry, auth_http_code=None,
209 210 initial_call_detection=False):
210 211 self.realm = realm
211 212 self.initial_call = initial_call_detection
212 213 self.authfunc = authfunc
213 214 self.registry = registry
214 215 self._rc_auth_http_code = auth_http_code
215 216
216 217 def _get_response_from_code(self, http_code):
217 218 try:
218 219 return get_exception(safe_int(http_code))
219 220 except Exception:
220 221 log.exception('Failed to fetch response for code %s' % http_code)
221 222 return HTTPForbidden
222 223
223 224 def build_authentication(self):
224 225 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
225 226 if self._rc_auth_http_code and not self.initial_call:
226 227 # return alternative HTTP code if alternative http return code
227 228 # is specified in RhodeCode config, but ONLY if it's not the
228 229 # FIRST call
229 230 custom_response_klass = self._get_response_from_code(
230 231 self._rc_auth_http_code)
231 232 return custom_response_klass(headers=head)
232 233 return HTTPUnauthorized(headers=head)
233 234
234 235 def authenticate(self, environ):
235 236 authorization = AUTHORIZATION(environ)
236 237 if not authorization:
237 238 return self.build_authentication()
238 239 (authmeth, auth) = authorization.split(' ', 1)
239 240 if 'basic' != authmeth.lower():
240 241 return self.build_authentication()
241 242 auth = auth.strip().decode('base64')
242 243 _parts = auth.split(':', 1)
243 244 if len(_parts) == 2:
244 245 username, password = _parts
245 246 if self.authfunc(
246 247 username, password, environ, VCS_TYPE,
247 248 registry=self.registry):
248 249 return username
249 250 if username and password:
250 251 # we mark that we actually executed authentication once, at
251 252 # that point we can use the alternative auth code
252 253 self.initial_call = False
253 254
254 255 return self.build_authentication()
255 256
256 257 __call__ = authenticate
257 258
258 259
259 260 def attach_context_attributes(context, request):
260 261 """
261 262 Attach variables into template context called `c`, please note that
262 263 request could be pylons or pyramid request in here.
263 264 """
264 265 rc_config = SettingsModel().get_all_settings(cache=True)
265 266
266 267 context.rhodecode_version = rhodecode.__version__
267 268 context.rhodecode_edition = config.get('rhodecode.edition')
268 269 # unique secret + version does not leak the version but keep consistency
269 270 context.rhodecode_version_hash = md5(
270 271 config.get('beaker.session.secret', '') +
271 272 rhodecode.__version__)[:8]
272 273
273 274 # Default language set for the incoming request
274 275 context.language = translation.get_lang()[0]
275 276
276 277 # Visual options
277 278 context.visual = AttributeDict({})
278 279
279 280 # DB stored Visual Items
280 281 context.visual.show_public_icon = str2bool(
281 282 rc_config.get('rhodecode_show_public_icon'))
282 283 context.visual.show_private_icon = str2bool(
283 284 rc_config.get('rhodecode_show_private_icon'))
284 285 context.visual.stylify_metatags = str2bool(
285 286 rc_config.get('rhodecode_stylify_metatags'))
286 287 context.visual.dashboard_items = safe_int(
287 288 rc_config.get('rhodecode_dashboard_items', 100))
288 289 context.visual.admin_grid_items = safe_int(
289 290 rc_config.get('rhodecode_admin_grid_items', 100))
290 291 context.visual.repository_fields = str2bool(
291 292 rc_config.get('rhodecode_repository_fields'))
292 293 context.visual.show_version = str2bool(
293 294 rc_config.get('rhodecode_show_version'))
294 295 context.visual.use_gravatar = str2bool(
295 296 rc_config.get('rhodecode_use_gravatar'))
296 297 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
297 298 context.visual.default_renderer = rc_config.get(
298 299 'rhodecode_markup_renderer', 'rst')
299 300 context.visual.rhodecode_support_url = \
300 301 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
301 302
302 303 context.pre_code = rc_config.get('rhodecode_pre_code')
303 304 context.post_code = rc_config.get('rhodecode_post_code')
304 305 context.rhodecode_name = rc_config.get('rhodecode_title')
305 306 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
306 307 # if we have specified default_encoding in the request, it has more
307 308 # priority
308 309 if request.GET.get('default_encoding'):
309 310 context.default_encodings.insert(0, request.GET.get('default_encoding'))
310 311 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
311 312
312 313 # INI stored
313 314 context.labs_active = str2bool(
314 315 config.get('labs_settings_active', 'false'))
315 316 context.visual.allow_repo_location_change = str2bool(
316 317 config.get('allow_repo_location_change', True))
317 318 context.visual.allow_custom_hooks_settings = str2bool(
318 319 config.get('allow_custom_hooks_settings', True))
319 320 context.debug_style = str2bool(config.get('debug_style', False))
320 321
321 322 context.rhodecode_instanceid = config.get('instance_id')
322 323
323 324 # AppEnlight
324 325 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
325 326 context.appenlight_api_public_key = config.get(
326 327 'appenlight.api_public_key', '')
327 328 context.appenlight_server_url = config.get('appenlight.server_url', '')
328 329
329 330 # JS template context
330 331 context.template_context = {
331 332 'repo_name': None,
332 333 'repo_type': None,
333 334 'repo_landing_commit': None,
334 335 'rhodecode_user': {
335 336 'username': None,
336 337 'email': None,
337 338 'notification_status': False
338 339 },
339 340 'visual': {
340 341 'default_renderer': None
341 342 },
342 343 'commit_data': {
343 344 'commit_id': None
344 345 },
345 346 'pull_request_data': {'pull_request_id': None},
346 347 'timeago': {
347 348 'refresh_time': 120 * 1000,
348 349 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
349 350 },
350 351 'pylons_dispatch': {
351 352 # 'controller': request.environ['pylons.routes_dict']['controller'],
352 353 # 'action': request.environ['pylons.routes_dict']['action'],
353 354 },
354 355 'pyramid_dispatch': {
355 356
356 357 },
357 358 'extra': {'plugins': {}}
358 359 }
359 360 # END CONFIG VARS
360 361
361 362 # TODO: This dosn't work when called from pylons compatibility tween.
362 363 # Fix this and remove it from base controller.
363 364 # context.repo_name = get_repo_slug(request) # can be empty
364 365
365 366 context.csrf_token = auth.get_csrf_token()
366 367 context.backends = rhodecode.BACKENDS.keys()
367 368 context.backends.sort()
368 369 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
369 370 context.rhodecode_user.user_id)
370 371
372 context.pyramid_request = pyramid.threadlocal.get_current_request()
373
371 374
372 375 def get_auth_user(environ):
373 376 ip_addr = get_ip_addr(environ)
374 377 # make sure that we update permissions each time we call controller
375 378 _auth_token = (request.GET.get('auth_token', '') or
376 379 request.GET.get('api_key', ''))
377 380
378 381 if _auth_token:
379 382 # when using API_KEY we are sure user exists.
380 383 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
381 384 authenticated = False
382 385 else:
383 386 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
384 387 try:
385 388 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
386 389 ip_addr=ip_addr)
387 390 except UserCreationError as e:
388 391 h.flash(e, 'error')
389 392 # container auth or other auth functions that create users
390 393 # on the fly can throw this exception signaling that there's
391 394 # issue with user creation, explanation should be provided
392 395 # in Exception itself. We then create a simple blank
393 396 # AuthUser
394 397 auth_user = AuthUser(ip_addr=ip_addr)
395 398
396 399 if password_changed(auth_user, session):
397 400 session.invalidate()
398 401 cookie_store = CookieStoreWrapper(
399 402 session.get('rhodecode_user'))
400 403 auth_user = AuthUser(ip_addr=ip_addr)
401 404
402 405 authenticated = cookie_store.get('is_authenticated')
403 406
404 407 if not auth_user.is_authenticated and auth_user.is_user_object:
405 408 # user is not authenticated and not empty
406 409 auth_user.set_authenticated(authenticated)
407 410
408 411 return auth_user
409 412
410 413
411 414 class BaseController(WSGIController):
412 415
413 416 def __before__(self):
414 417 """
415 418 __before__ is called before controller methods and after __call__
416 419 """
417 420 # on each call propagate settings calls into global settings.
418 421 set_rhodecode_config(config)
419 422 attach_context_attributes(c, request)
420 423
421 424 # TODO: Remove this when fixed in attach_context_attributes()
422 425 c.repo_name = get_repo_slug(request) # can be empty
423 426
424 427 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
425 428 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
426 429 self.sa = meta.Session
427 430 self.scm_model = ScmModel(self.sa)
428 431
429 432 default_lang = c.language
430 433 user_lang = c.language
431 434 try:
432 435 user_obj = self._rhodecode_user.get_instance()
433 436 if user_obj:
434 437 user_lang = user_obj.user_data.get('language')
435 438 except Exception:
436 439 log.exception('Failed to fetch user language for user %s',
437 440 self._rhodecode_user)
438 441
439 442 if user_lang and user_lang != default_lang:
440 443 log.debug('set language to %s for user %s', user_lang,
441 444 self._rhodecode_user)
442 445 translation.set_lang(user_lang)
443 446
444 447 def _dispatch_redirect(self, with_url, environ, start_response):
445 448 resp = HTTPFound(with_url)
446 449 environ['SCRIPT_NAME'] = '' # handle prefix middleware
447 450 environ['PATH_INFO'] = with_url
448 451 return resp(environ, start_response)
449 452
450 453 def __call__(self, environ, start_response):
451 454 """Invoke the Controller"""
452 455 # WSGIController.__call__ dispatches to the Controller method
453 456 # the request is routed to. This routing information is
454 457 # available in environ['pylons.routes_dict']
455 458 from rhodecode.lib import helpers as h
456 459
457 460 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
458 461 if environ.get('debugtoolbar.wants_pylons_context', False):
459 462 environ['debugtoolbar.pylons_context'] = c._current_obj()
460 463
461 464 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
462 465 environ['pylons.routes_dict']['action']])
463 466
464 467 self.rc_config = SettingsModel().get_all_settings(cache=True)
465 468 self.ip_addr = get_ip_addr(environ)
466 469
467 470 # The rhodecode auth user is looked up and passed through the
468 471 # environ by the pylons compatibility tween in pyramid.
469 472 # So we can just grab it from there.
470 473 auth_user = environ['rc_auth_user']
471 474
472 475 # set globals for auth user
473 476 request.user = auth_user
474 477 c.rhodecode_user = self._rhodecode_user = auth_user
475 478
476 479 log.info('IP: %s User: %s accessed %s [%s]' % (
477 480 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
478 481 _route_name)
479 482 )
480 483
481 484 # TODO: Maybe this should be move to pyramid to cover all views.
482 485 # check user attributes for password change flag
483 486 user_obj = auth_user.get_instance()
484 487 if user_obj and user_obj.user_data.get('force_password_change'):
485 488 h.flash('You are required to change your password', 'warning',
486 489 ignore_duplicate=True)
487 490
488 491 skip_user_check_urls = [
489 492 'error.document', 'login.logout', 'login.index',
490 493 'admin/my_account.my_account_password',
491 494 'admin/my_account.my_account_password_update'
492 495 ]
493 496 if _route_name not in skip_user_check_urls:
494 497 return self._dispatch_redirect(
495 498 url('my_account_password'), environ, start_response)
496 499
497 500 return WSGIController.__call__(self, environ, start_response)
498 501
499 502
500 503 class BaseRepoController(BaseController):
501 504 """
502 505 Base class for controllers responsible for loading all needed data for
503 506 repository loaded items are
504 507
505 508 c.rhodecode_repo: instance of scm repository
506 509 c.rhodecode_db_repo: instance of db
507 510 c.repository_requirements_missing: shows that repository specific data
508 511 could not be displayed due to the missing requirements
509 512 c.repository_pull_requests: show number of open pull requests
510 513 """
511 514
512 515 def __before__(self):
513 516 super(BaseRepoController, self).__before__()
514 517 if c.repo_name: # extracted from routes
515 518 db_repo = Repository.get_by_repo_name(c.repo_name)
516 519 if not db_repo:
517 520 return
518 521
519 522 log.debug(
520 523 'Found repository in database %s with state `%s`',
521 524 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
522 525 route = getattr(request.environ.get('routes.route'), 'name', '')
523 526
524 527 # allow to delete repos that are somehow damages in filesystem
525 528 if route in ['delete_repo']:
526 529 return
527 530
528 531 if db_repo.repo_state in [Repository.STATE_PENDING]:
529 532 if route in ['repo_creating_home']:
530 533 return
531 534 check_url = url('repo_creating_home', repo_name=c.repo_name)
532 535 return redirect(check_url)
533 536
534 537 self.rhodecode_db_repo = db_repo
535 538
536 539 missing_requirements = False
537 540 try:
538 541 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
539 542 except RepositoryRequirementError as e:
540 543 missing_requirements = True
541 544 self._handle_missing_requirements(e)
542 545
543 546 if self.rhodecode_repo is None and not missing_requirements:
544 547 log.error('%s this repository is present in database but it '
545 548 'cannot be created as an scm instance', c.repo_name)
546 549
547 550 h.flash(_(
548 551 "The repository at %(repo_name)s cannot be located.") %
549 552 {'repo_name': c.repo_name},
550 553 category='error', ignore_duplicate=True)
551 554 redirect(url('home'))
552 555
553 556 # update last change according to VCS data
554 557 if not missing_requirements:
555 558 commit = db_repo.get_commit(
556 559 pre_load=["author", "date", "message", "parents"])
557 560 db_repo.update_commit_cache(commit)
558 561
559 562 # Prepare context
560 563 c.rhodecode_db_repo = db_repo
561 564 c.rhodecode_repo = self.rhodecode_repo
562 565 c.repository_requirements_missing = missing_requirements
563 566
564 567 self._update_global_counters(self.scm_model, db_repo)
565 568
566 569 def _update_global_counters(self, scm_model, db_repo):
567 570 """
568 571 Base variables that are exposed to every page of repository
569 572 """
570 573 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
571 574
572 575 def _handle_missing_requirements(self, error):
573 576 self.rhodecode_repo = None
574 577 log.error(
575 578 'Requirements are missing for repository %s: %s',
576 579 c.repo_name, error.message)
577 580
578 581 summary_url = url('summary_home', repo_name=c.repo_name)
579 582 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
580 583 settings_update_url = url('repo', repo_name=c.repo_name)
581 584 path = request.path
582 585 should_redirect = (
583 586 path not in (summary_url, settings_update_url)
584 587 and '/settings' not in path or path == statistics_url
585 588 )
586 589 if should_redirect:
587 590 redirect(summary_url)
General Comments 0
You need to be logged in to leave comments. Login now