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