##// END OF EJS Templates
auth: don't trust clients too much - only trust the *last* IP in the X-Forwarded-For header...
Mads Kiilerich -
r8678:f08fbf42 default
parent child Browse files
Show More
@@ -1,639 +1,641 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14
15 15 """
16 16 kallithea.controllers.base
17 17 ~~~~~~~~~~~~~~~~~~~~~~~~~~
18 18
19 19 The base Controller API
20 20 Provides the BaseController class for subclassing. And usage in different
21 21 controllers
22 22
23 23 This file was forked by the Kallithea project in July 2014.
24 24 Original author and date, and relevant copyright and licensing information is below:
25 25 :created_on: Oct 06, 2010
26 26 :author: marcink
27 27 :copyright: (c) 2013 RhodeCode GmbH, and others.
28 28 :license: GPLv3, see LICENSE.md for more details.
29 29 """
30 30
31 31 import base64
32 32 import datetime
33 33 import logging
34 34 import traceback
35 35 import warnings
36 36
37 37 import decorator
38 38 import paste.auth.basic
39 39 import paste.httpexceptions
40 40 import paste.httpheaders
41 41 import webob.exc
42 42 from tg import TGController, config, render_template, request, response, session
43 43 from tg import tmpl_context as c
44 44 from tg.i18n import ugettext as _
45 45
46 46 import kallithea
47 47 from kallithea.lib import auth_modules, ext_json, webutils
48 48 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
49 49 from kallithea.lib.exceptions import UserCreationError
50 50 from kallithea.lib.utils import get_repo_slug, is_valid_repo
51 51 from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment
52 52 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
53 53 from kallithea.lib.webutils import url
54 54 from kallithea.model import db, meta
55 55 from kallithea.model.scm import ScmModel
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 def render(template_path):
62 62 return render_template({'url': url}, 'mako', template_path)
63 63
64 64
65 65 def _filter_proxy(ip):
66 66 """
67 HEADERS can have multiple ips inside the left-most being the original
68 client, and each successive proxy that passed the request adding the IP
69 address where it received the request from.
67 HTTP_X_FORWARDED_FOR headers can have multiple IP addresses, with the
68 leftmost being the original client. Each proxy that is forwarding the
69 request will usually add the IP address it sees the request coming from.
70 70
71 :param ip:
71 The client might have provided a fake leftmost value before hitting the
72 first proxy, so if we have a proxy that is adding one IP address, we can
73 only trust the rightmost address.
72 74 """
73 75 if ',' in ip:
74 76 _ips = ip.split(',')
75 _first_ip = _ips[0].strip()
77 _first_ip = _ips[-1].strip()
76 78 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
77 79 return _first_ip
78 80 return ip
79 81
80 82
81 83 def get_ip_addr(environ):
82 84 proxy_key = 'HTTP_X_REAL_IP'
83 85 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
84 86 def_key = 'REMOTE_ADDR'
85 87
86 88 ip = environ.get(proxy_key)
87 89 if ip:
88 90 return _filter_proxy(ip)
89 91
90 92 ip = environ.get(proxy_key2)
91 93 if ip:
92 94 return _filter_proxy(ip)
93 95
94 96 ip = environ.get(def_key, '0.0.0.0')
95 97 return _filter_proxy(ip)
96 98
97 99
98 100 def get_path_info(environ):
99 101 """Return PATH_INFO from environ ... using tg.original_request if available.
100 102
101 103 In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded
102 104 bytes. The code points are guaranteed to only use the lower 8 bit bits, and
103 105 encoding the string with the 1:1 encoding latin1 will give the
104 106 corresponding byte string ... which then can be decoded to proper unicode.
105 107 """
106 108 org_req = environ.get('tg.original_request')
107 109 if org_req is not None:
108 110 environ = org_req.environ
109 111 return safe_str(environ['PATH_INFO'].encode('latin1'))
110 112
111 113
112 114 def log_in_user(user, remember, is_external_auth, ip_addr):
113 115 """
114 116 Log a `User` in and update session and cookies. If `remember` is True,
115 117 the session cookie is set to expire in a year; otherwise, it expires at
116 118 the end of the browser session.
117 119
118 120 Returns populated `AuthUser` object.
119 121 """
120 122 # It should not be possible to explicitly log in as the default user.
121 123 assert not user.is_default_user, user
122 124
123 125 auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
124 126 if auth_user is None:
125 127 return None
126 128
127 129 user.update_lastlogin()
128 130 meta.Session().commit()
129 131
130 132 # Start new session to prevent session fixation attacks.
131 133 session.invalidate()
132 134 session['authuser'] = cookie = auth_user.to_cookie()
133 135
134 136 # If they want to be remembered, update the cookie.
135 137 # NOTE: Assumes that beaker defaults to browser session cookie.
136 138 if remember:
137 139 t = datetime.datetime.now() + datetime.timedelta(days=365)
138 140 session._set_cookie_expires(t)
139 141
140 142 session.save()
141 143
142 144 log.info('user %s is now authenticated and stored in '
143 145 'session, session attrs %s', user.username, cookie)
144 146
145 147 # dumps session attrs back to cookie
146 148 session._update_cookie_out()
147 149
148 150 return auth_user
149 151
150 152
151 153 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
152 154
153 155 def __init__(self, realm, authfunc, auth_http_code=None):
154 156 self.realm = realm
155 157 self.authfunc = authfunc
156 158 self._rc_auth_http_code = auth_http_code
157 159
158 160 def build_authentication(self, environ):
159 161 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
160 162 # Consume the whole body before sending a response
161 163 try:
162 164 request_body_size = int(environ.get('CONTENT_LENGTH', 0))
163 165 except (ValueError):
164 166 request_body_size = 0
165 167 environ['wsgi.input'].read(request_body_size)
166 168 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
167 169 # return 403 if alternative http return code is specified in
168 170 # Kallithea config
169 171 return paste.httpexceptions.HTTPForbidden(headers=head)
170 172 return paste.httpexceptions.HTTPUnauthorized(headers=head)
171 173
172 174 def authenticate(self, environ):
173 175 authorization = paste.httpheaders.AUTHORIZATION(environ)
174 176 if not authorization:
175 177 return self.build_authentication(environ)
176 178 (authmeth, auth) = authorization.split(' ', 1)
177 179 if 'basic' != authmeth.lower():
178 180 return self.build_authentication(environ)
179 181 auth = safe_str(base64.b64decode(auth.strip()))
180 182 _parts = auth.split(':', 1)
181 183 if len(_parts) == 2:
182 184 username, password = _parts
183 185 if self.authfunc(username, password, environ) is not None:
184 186 return username
185 187 return self.build_authentication(environ)
186 188
187 189 __call__ = authenticate
188 190
189 191
190 192 class BaseVCSController(object):
191 193 """Base controller for handling Mercurial/Git protocol requests
192 194 (coming from a VCS client, and not a browser).
193 195 """
194 196
195 197 scm_alias = None # 'hg' / 'git'
196 198
197 199 def __init__(self, application, config):
198 200 self.application = application
199 201 self.config = config
200 202 # base path of repo locations
201 203 self.basepath = self.config['base_path']
202 204 # authenticate this VCS request using the authentication modules
203 205 self.authenticate = BasicAuth('', auth_modules.authenticate,
204 206 config.get('auth_ret_code'))
205 207
206 208 @classmethod
207 209 def parse_request(cls, environ):
208 210 """If request is parsed as a request for this VCS, return a namespace with the parsed request.
209 211 If the request is unknown, return None.
210 212 """
211 213 raise NotImplementedError()
212 214
213 215 def _authorize(self, environ, action, repo_name, ip_addr):
214 216 """Authenticate and authorize user.
215 217
216 218 Since we're dealing with a VCS client and not a browser, we only
217 219 support HTTP basic authentication, either directly via raw header
218 220 inspection, or by using container authentication to delegate the
219 221 authentication to the web server.
220 222
221 223 Returns (user, None) on successful authentication and authorization.
222 224 Returns (None, wsgi_app) to send the wsgi_app response to the client.
223 225 """
224 226 # Use anonymous access if allowed for action on repo.
225 227 default_user = db.User.get_default_user()
226 228 default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
227 229 if default_authuser is None:
228 230 log.debug('No anonymous access at all') # move on to proper user auth
229 231 else:
230 232 if self._check_permission(action, default_authuser, repo_name):
231 233 return default_authuser, None
232 234 log.debug('Not authorized to access this repository as anonymous user')
233 235
234 236 username = None
235 237 #==============================================================
236 238 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
237 239 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
238 240 #==============================================================
239 241
240 242 # try to auth based on environ, container auth methods
241 243 log.debug('Running PRE-AUTH for container based authentication')
242 244 pre_auth = auth_modules.authenticate('', '', environ)
243 245 if pre_auth is not None and pre_auth.get('username'):
244 246 username = pre_auth['username']
245 247 log.debug('PRE-AUTH got %s as username', username)
246 248
247 249 # If not authenticated by the container, running basic auth
248 250 if not username:
249 251 self.authenticate.realm = self.config['realm']
250 252 result = self.authenticate(environ)
251 253 if isinstance(result, str):
252 254 paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
253 255 paste.httpheaders.REMOTE_USER.update(environ, result)
254 256 username = result
255 257 else:
256 258 return None, result.wsgi_application
257 259
258 260 #==============================================================
259 261 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
260 262 #==============================================================
261 263 try:
262 264 user = db.User.get_by_username_or_email(username)
263 265 except Exception:
264 266 log.error(traceback.format_exc())
265 267 return None, webob.exc.HTTPInternalServerError()
266 268
267 269 authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr)
268 270 if authuser is None:
269 271 return None, webob.exc.HTTPForbidden()
270 272 if not self._check_permission(action, authuser, repo_name):
271 273 return None, webob.exc.HTTPForbidden()
272 274
273 275 return user, None
274 276
275 277 def _handle_request(self, environ, start_response):
276 278 raise NotImplementedError()
277 279
278 280 def _check_permission(self, action, authuser, repo_name):
279 281 """
280 282 :param action: 'push' or 'pull'
281 283 :param user: `AuthUser` instance
282 284 :param repo_name: repository name
283 285 """
284 286 if action == 'push':
285 287 if not HasPermissionAnyMiddleware('repository.write',
286 288 'repository.admin')(authuser,
287 289 repo_name):
288 290 return False
289 291
290 292 elif action == 'pull':
291 293 #any other action need at least read permission
292 294 if not HasPermissionAnyMiddleware('repository.read',
293 295 'repository.write',
294 296 'repository.admin')(authuser,
295 297 repo_name):
296 298 return False
297 299
298 300 else:
299 301 assert False, action
300 302
301 303 return True
302 304
303 305 def __call__(self, environ, start_response):
304 306 try:
305 307 # try parsing a request for this VCS - if it fails, call the wrapped app
306 308 parsed_request = self.parse_request(environ)
307 309 if parsed_request is None:
308 310 return self.application(environ, start_response)
309 311
310 312 # skip passing error to error controller
311 313 environ['pylons.status_code_redirect'] = True
312 314
313 315 # quick check if repo exists...
314 316 if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
315 317 raise webob.exc.HTTPNotFound()
316 318
317 319 if parsed_request.action is None:
318 320 # Note: the client doesn't get the helpful error message
319 321 raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
320 322
321 323 #======================================================================
322 324 # CHECK PERMISSIONS
323 325 #======================================================================
324 326 ip_addr = get_ip_addr(environ)
325 327 user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
326 328 if response_app is not None:
327 329 return response_app(environ, start_response)
328 330
329 331 #======================================================================
330 332 # REQUEST HANDLING
331 333 #======================================================================
332 334 set_hook_environment(user.username, ip_addr,
333 335 parsed_request.repo_name, self.scm_alias, parsed_request.action)
334 336
335 337 try:
336 338 log.info('%s action on %s repo "%s" by "%s" from %s',
337 339 parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
338 340 app = self._make_app(parsed_request)
339 341 return app(environ, start_response)
340 342 except Exception:
341 343 log.error(traceback.format_exc())
342 344 raise webob.exc.HTTPInternalServerError()
343 345
344 346 except webob.exc.HTTPException as e:
345 347 return e(environ, start_response)
346 348
347 349
348 350 class BaseController(TGController):
349 351
350 352 def _before(self, *args, **kwargs):
351 353 """
352 354 _before is called before controller methods and after __call__
353 355 """
354 356 if request.needs_csrf_check:
355 357 # CSRF protection: Whenever a request has ambient authority (whether
356 358 # through a session cookie or its origin IP address), it must include
357 359 # the correct token, unless the HTTP method is GET or HEAD (and thus
358 360 # guaranteed to be side effect free. In practice, the only situation
359 361 # where we allow side effects without ambient authority is when the
360 362 # authority comes from an API key; and that is handled above.
361 363 token = request.POST.get(webutils.session_csrf_secret_name)
362 364 if not token or token != webutils.session_csrf_secret_token():
363 365 log.error('CSRF check failed')
364 366 raise webob.exc.HTTPForbidden()
365 367
366 368 c.kallithea_version = kallithea.__version__
367 369 settings = db.Setting.get_app_settings()
368 370
369 371 # Visual options
370 372 c.visual = AttributeDict({})
371 373
372 374 ## DB stored
373 375 c.visual.show_public_icon = asbool(settings.get('show_public_icon'))
374 376 c.visual.show_private_icon = asbool(settings.get('show_private_icon'))
375 377 c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels'))
376 378 c.visual.page_size = safe_int(settings.get('dashboard_items', 100))
377 379 c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100))
378 380 c.visual.repository_fields = asbool(settings.get('repository_fields'))
379 381 c.visual.show_version = asbool(settings.get('show_version'))
380 382 c.visual.use_gravatar = asbool(settings.get('use_gravatar'))
381 383 c.visual.gravatar_url = settings.get('gravatar_url')
382 384
383 385 c.ga_code = settings.get('ga_code')
384 386 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
385 387 if c.ga_code and '<' not in c.ga_code:
386 388 c.ga_code = '''<script type="text/javascript">
387 389 var _gaq = _gaq || [];
388 390 _gaq.push(['_setAccount', '%s']);
389 391 _gaq.push(['_trackPageview']);
390 392
391 393 (function() {
392 394 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
393 395 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
394 396 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
395 397 })();
396 398 </script>''' % c.ga_code
397 399 c.site_name = settings.get('title')
398 400 c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI
399 401 c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH
400 402
401 403 ## INI stored
402 404 c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True))
403 405 c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True))
404 406 c.ssh_enabled = asbool(config.get('ssh_enabled', False))
405 407
406 408 c.instance_id = config.get('instance_id')
407 409 c.issues_url = config.get('bugtracker', url('issues_url'))
408 410 # END CONFIG VARS
409 411
410 412 c.repo_name = get_repo_slug(request) # can be empty
411 413 c.backends = list(kallithea.BACKENDS)
412 414
413 415 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
414 416
415 417 c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count()
416 418
417 419 self.scm_model = ScmModel()
418 420
419 421 @staticmethod
420 422 def _determine_auth_user(session_authuser, ip_addr):
421 423 """
422 424 Create an `AuthUser` object given the API key/bearer token
423 425 (if any) and the value of the authuser session cookie.
424 426 Returns None if no valid user is found (like not active or no access for IP).
425 427 """
426 428
427 429 # Authenticate by session cookie
428 430 # In ancient login sessions, 'authuser' may not be a dict.
429 431 # In that case, the user will have to log in again.
430 432 # v0.3 and earlier included an 'is_authenticated' key; if present,
431 433 # this must be True.
432 434 if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True):
433 435 return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr)
434 436
435 437 # Authenticate by auth_container plugin (if enabled)
436 438 if any(
437 439 plugin.is_container_auth
438 440 for plugin in auth_modules.get_auth_plugins()
439 441 ):
440 442 try:
441 443 user_info = auth_modules.authenticate('', '', request.environ)
442 444 except UserCreationError as e:
443 445 webutils.flash(e, 'error', logf=log.error)
444 446 else:
445 447 if user_info is not None:
446 448 username = user_info['username']
447 449 user = db.User.get_by_username(username, case_insensitive=True)
448 450 return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
449 451
450 452 # User is default user (if active) or anonymous
451 453 default_user = db.User.get_default_user()
452 454 authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
453 455 if authuser is None: # fall back to anonymous
454 456 authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
455 457 return authuser
456 458
457 459 @staticmethod
458 460 def _basic_security_checks():
459 461 """Perform basic security/sanity checks before processing the request."""
460 462
461 463 # Only allow the following HTTP request methods.
462 464 if request.method not in ['GET', 'HEAD', 'POST']:
463 465 raise webob.exc.HTTPMethodNotAllowed()
464 466
465 467 # Also verify the _method override - no longer allowed.
466 468 if request.params.get('_method') is None:
467 469 pass # no override, no problem
468 470 else:
469 471 raise webob.exc.HTTPMethodNotAllowed()
470 472
471 473 # Make sure CSRF token never appears in the URL. If so, invalidate it.
472 474 if webutils.session_csrf_secret_name in request.GET:
473 475 log.error('CSRF key leak detected')
474 476 session.pop(webutils.session_csrf_secret_name, None)
475 477 session.save()
476 478 webutils.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
477 479 category='error')
478 480
479 481 # WebOb already ignores request payload parameters for anything other
480 482 # than POST/PUT, but double-check since other Kallithea code relies on
481 483 # this assumption.
482 484 if request.method not in ['POST', 'PUT'] and request.POST:
483 485 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
484 486 raise webob.exc.HTTPBadRequest()
485 487
486 488 def __call__(self, environ, context):
487 489 try:
488 490 ip_addr = get_ip_addr(environ)
489 491 self._basic_security_checks()
490 492
491 493 api_key = request.GET.get('api_key')
492 494 try:
493 495 # Request.authorization may raise ValueError on invalid input
494 496 type, params = request.authorization
495 497 except (ValueError, TypeError):
496 498 pass
497 499 else:
498 500 if type.lower() == 'bearer':
499 501 api_key = params # bearer token is an api key too
500 502
501 503 if api_key is None:
502 504 authuser = self._determine_auth_user(
503 505 session.get('authuser'),
504 506 ip_addr=ip_addr,
505 507 )
506 508 needs_csrf_check = request.method not in ['GET', 'HEAD']
507 509
508 510 else:
509 511 dbuser = db.User.get_by_api_key(api_key)
510 512 if dbuser is None:
511 513 log.info('No db user found for authentication with API key ****%s from %s',
512 514 api_key[-4:], ip_addr)
513 515 authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr)
514 516 needs_csrf_check = False # API key provides CSRF protection
515 517
516 518 if authuser is None:
517 519 log.info('No valid user found')
518 520 raise webob.exc.HTTPForbidden()
519 521
520 522 # set globals for auth user
521 523 request.authuser = authuser
522 524 request.ip_addr = ip_addr
523 525 request.needs_csrf_check = needs_csrf_check
524 526
525 527 log.info('IP: %s User: %s Request: %s',
526 528 request.ip_addr, request.authuser,
527 529 get_path_info(environ),
528 530 )
529 531 return super(BaseController, self).__call__(environ, context)
530 532 except webob.exc.HTTPException as e:
531 533 return e
532 534
533 535
534 536 class BaseRepoController(BaseController):
535 537 """
536 538 Base class for controllers responsible for loading all needed data for
537 539 repository loaded items are
538 540
539 541 c.db_repo_scm_instance: instance of scm repository
540 542 c.db_repo: instance of db
541 543 c.repository_followers: number of followers
542 544 c.repository_forks: number of forks
543 545 c.repository_following: weather the current user is following the current repo
544 546 """
545 547
546 548 def _before(self, *args, **kwargs):
547 549 super(BaseRepoController, self)._before(*args, **kwargs)
548 550 if c.repo_name: # extracted from request by base-base BaseController._before
549 551 _dbr = db.Repository.get_by_repo_name(c.repo_name)
550 552 if not _dbr:
551 553 return
552 554
553 555 log.debug('Found repository in database %s with state `%s`',
554 556 _dbr, _dbr.repo_state)
555 557 route = getattr(request.environ.get('routes.route'), 'name', '')
556 558
557 559 # allow to delete repos that are somehow damages in filesystem
558 560 if route in ['delete_repo']:
559 561 return
560 562
561 563 if _dbr.repo_state in [db.Repository.STATE_PENDING]:
562 564 if route in ['repo_creating_home']:
563 565 return
564 566 check_url = url('repo_creating_home', repo_name=c.repo_name)
565 567 raise webob.exc.HTTPFound(location=check_url)
566 568
567 569 dbr = c.db_repo = _dbr
568 570 c.db_repo_scm_instance = c.db_repo.scm_instance
569 571 if c.db_repo_scm_instance is None:
570 572 log.error('%s this repository is present in database but it '
571 573 'cannot be created as an scm instance', c.repo_name)
572 574 webutils.flash(_('Repository not found in the filesystem'),
573 575 category='error')
574 576 raise webob.exc.HTTPNotFound()
575 577
576 578 # some globals counter for menu
577 579 c.repository_followers = self.scm_model.get_followers(dbr)
578 580 c.repository_forks = self.scm_model.get_forks(dbr)
579 581 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
580 582 c.repository_following = self.scm_model.is_following_repo(
581 583 c.repo_name, request.authuser.user_id)
582 584
583 585 @staticmethod
584 586 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
585 587 """
586 588 Safe way to get changeset. If error occurs show error.
587 589 """
588 590 try:
589 591 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
590 592 except EmptyRepositoryError as e:
591 593 if returnempty:
592 594 return repo.scm_instance.EMPTY_CHANGESET
593 595 webutils.flash(_('There are no changesets yet'), category='error')
594 596 raise webob.exc.HTTPNotFound()
595 597 except ChangesetDoesNotExistError as e:
596 598 webutils.flash(_('Changeset for %s %s not found in %s') %
597 599 (ref_type, ref_name, repo.repo_name),
598 600 category='error')
599 601 raise webob.exc.HTTPNotFound()
600 602 except RepositoryError as e:
601 603 log.error(traceback.format_exc())
602 604 webutils.flash(e, category='error')
603 605 raise webob.exc.HTTPBadRequest()
604 606
605 607
606 608 @decorator.decorator
607 609 def jsonify(func, *args, **kwargs):
608 610 """Action decorator that formats output for JSON
609 611
610 612 Given a function that will return content, this decorator will turn
611 613 the result into JSON, with a content-type of 'application/json' and
612 614 output it.
613 615 """
614 616 response.headers['Content-Type'] = 'application/json; charset=utf-8'
615 617 data = func(*args, **kwargs)
616 618 if isinstance(data, (list, tuple)):
617 619 # A JSON list response is syntactically valid JavaScript and can be
618 620 # loaded and executed as JavaScript by a malicious third-party site
619 621 # using <script>, which can lead to cross-site data leaks.
620 622 # JSON responses should therefore be scalars or objects (i.e. Python
621 623 # dicts), because a JSON object is a syntax error if intepreted as JS.
622 624 msg = "JSON responses with Array envelopes are susceptible to " \
623 625 "cross-site data leak attacks, see " \
624 626 "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
625 627 warnings.warn(msg, Warning, 2)
626 628 log.warning(msg)
627 629 log.debug("Returning JSON wrapped action output")
628 630 return ascii_bytes(ext_json.dumps(data))
629 631
630 632 @decorator.decorator
631 633 def IfSshEnabled(func, *args, **kwargs):
632 634 """Decorator for functions that can only be called if SSH access is enabled.
633 635
634 636 If SSH access is disabled in the configuration file, HTTPNotFound is raised.
635 637 """
636 638 if not c.ssh_enabled:
637 639 webutils.flash(_("SSH access is disabled."), category='warning')
638 640 raise webob.exc.HTTPNotFound()
639 641 return func(*args, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now