##// END OF EJS Templates
core: always attach pyramid context into request...
marcink -
r1896:936e31fc default
parent child Browse files
Show More
@@ -1,542 +1,542 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 import inspect
22 22 import itertools
23 23 import logging
24 24 import types
25 25 import fnmatch
26 26
27 27 import decorator
28 28 import venusian
29 29 from collections import OrderedDict
30 30
31 31 from pyramid.exceptions import ConfigurationError
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34 from pyramid.httpexceptions import HTTPNotFound
35 35
36 36 from rhodecode.api.exc import (
37 37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 38 from rhodecode.apps._base import TemplateArgs
39 39 from rhodecode.lib.auth import AuthUser
40 40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.utils2 import safe_str
43 43 from rhodecode.lib.plugins.utils import get_plugin_settings
44 44 from rhodecode.model.db import User, UserApiKeys
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48 DEFAULT_RENDERER = 'jsonrpc_renderer'
49 49 DEFAULT_URL = '/_admin/apiv2'
50 50
51 51
52 52 def find_methods(jsonrpc_methods, pattern):
53 53 matches = OrderedDict()
54 54 if not isinstance(pattern, (list, tuple)):
55 55 pattern = [pattern]
56 56
57 57 for single_pattern in pattern:
58 58 for method_name, method in jsonrpc_methods.items():
59 59 if fnmatch.fnmatch(method_name, single_pattern):
60 60 matches[method_name] = method
61 61 return matches
62 62
63 63
64 64 class ExtJsonRenderer(object):
65 65 """
66 66 Custom renderer that mkaes use of our ext_json lib
67 67
68 68 """
69 69
70 70 def __init__(self, serializer=json.dumps, **kw):
71 71 """ Any keyword arguments will be passed to the ``serializer``
72 72 function."""
73 73 self.serializer = serializer
74 74 self.kw = kw
75 75
76 76 def __call__(self, info):
77 77 """ Returns a plain JSON-encoded string with content-type
78 78 ``application/json``. The content-type may be overridden by
79 79 setting ``request.response.content_type``."""
80 80
81 81 def _render(value, system):
82 82 request = system.get('request')
83 83 if request is not None:
84 84 response = request.response
85 85 ct = response.content_type
86 86 if ct == response.default_content_type:
87 87 response.content_type = 'application/json'
88 88
89 89 return self.serializer(value, **self.kw)
90 90
91 91 return _render
92 92
93 93
94 94 def jsonrpc_response(request, result):
95 95 rpc_id = getattr(request, 'rpc_id', None)
96 96 response = request.response
97 97
98 98 # store content_type before render is called
99 99 ct = response.content_type
100 100
101 101 ret_value = ''
102 102 if rpc_id:
103 103 ret_value = {
104 104 'id': rpc_id,
105 105 'result': result,
106 106 'error': None,
107 107 }
108 108
109 109 # fetch deprecation warnings, and store it inside results
110 110 deprecation = getattr(request, 'rpc_deprecation', None)
111 111 if deprecation:
112 112 ret_value['DEPRECATION_WARNING'] = deprecation
113 113
114 114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
115 115 response.body = safe_str(raw_body, response.charset)
116 116
117 117 if ct == response.default_content_type:
118 118 response.content_type = 'application/json'
119 119
120 120 return response
121 121
122 122
123 123 def jsonrpc_error(request, message, retid=None, code=None):
124 124 """
125 125 Generate a Response object with a JSON-RPC error body
126 126
127 127 :param code:
128 128 :param retid:
129 129 :param message:
130 130 """
131 131 err_dict = {'id': retid, 'result': None, 'error': message}
132 132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
133 133 return Response(
134 134 body=body,
135 135 status=code,
136 136 content_type='application/json'
137 137 )
138 138
139 139
140 140 def exception_view(exc, request):
141 141 rpc_id = getattr(request, 'rpc_id', None)
142 142
143 143 fault_message = 'undefined error'
144 144 if isinstance(exc, JSONRPCError):
145 145 fault_message = exc.message
146 146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
147 147 elif isinstance(exc, JSONRPCValidationError):
148 148 colander_exc = exc.colander_exception
149 149 # TODO(marcink): think maybe of nicer way to serialize errors ?
150 150 fault_message = colander_exc.asdict()
151 151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
152 152 elif isinstance(exc, JSONRPCForbidden):
153 153 fault_message = 'Access was denied to this resource.'
154 154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
155 155 elif isinstance(exc, HTTPNotFound):
156 156 method = request.rpc_method
157 157 log.debug('json-rpc method `%s` not found in list of '
158 158 'api calls: %s, rpc_id:%s',
159 159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
160 160
161 161 similar = 'none'
162 162 try:
163 163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
164 164 similar_found = find_methods(
165 165 request.registry.jsonrpc_methods, similar_paterns)
166 166 similar = ', '.join(similar_found.keys()) or similar
167 167 except Exception:
168 168 # make the whole above block safe
169 169 pass
170 170
171 171 fault_message = "No such method: {}. Similar methods: {}".format(
172 172 method, similar)
173 173
174 174 return jsonrpc_error(request, fault_message, rpc_id)
175 175
176 176
177 177 def request_view(request):
178 178 """
179 179 Main request handling method. It handles all logic to call a specific
180 180 exposed method
181 181 """
182 182
183 183 # check if we can find this session using api_key, get_by_auth_token
184 184 # search not expired tokens only
185 185
186 186 try:
187 187 api_user = User.get_by_auth_token(request.rpc_api_key)
188 188
189 189 if api_user is None:
190 190 return jsonrpc_error(
191 191 request, retid=request.rpc_id, message='Invalid API KEY')
192 192
193 193 if not api_user.active:
194 194 return jsonrpc_error(
195 195 request, retid=request.rpc_id,
196 196 message='Request from this user not allowed')
197 197
198 198 # check if we are allowed to use this IP
199 199 auth_u = AuthUser(
200 200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
201 201 if not auth_u.ip_allowed:
202 202 return jsonrpc_error(
203 203 request, retid=request.rpc_id,
204 204 message='Request from IP:%s not allowed' % (
205 205 request.rpc_ip_addr,))
206 206 else:
207 207 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
208 208
209 209 # register our auth-user
210 210 request.rpc_user = auth_u
211 211
212 212 # now check if token is valid for API
213 213 auth_token = request.rpc_api_key
214 214 token_match = api_user.authenticate_by_token(
215 215 auth_token, roles=[UserApiKeys.ROLE_API])
216 216 invalid_token = not token_match
217 217
218 218 log.debug('Checking if API KEY is valid with proper role')
219 219 if invalid_token:
220 220 return jsonrpc_error(
221 221 request, retid=request.rpc_id,
222 222 message='API KEY invalid or, has bad role for an API call')
223 223
224 224 except Exception:
225 225 log.exception('Error on API AUTH')
226 226 return jsonrpc_error(
227 227 request, retid=request.rpc_id, message='Invalid API KEY')
228 228
229 229 method = request.rpc_method
230 230 func = request.registry.jsonrpc_methods[method]
231 231
232 232 # now that we have a method, add request._req_params to
233 233 # self.kargs and dispatch control to WGIController
234 234 argspec = inspect.getargspec(func)
235 235 arglist = argspec[0]
236 236 defaults = map(type, argspec[3] or [])
237 237 default_empty = types.NotImplementedType
238 238
239 239 # kw arguments required by this method
240 240 func_kwargs = dict(itertools.izip_longest(
241 241 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242 242
243 243 # This attribute will need to be first param of a method that uses
244 244 # api_key, which is translated to instance of user at that name
245 245 user_var = 'apiuser'
246 246 request_var = 'request'
247 247
248 248 for arg in [user_var, request_var]:
249 249 if arg not in arglist:
250 250 return jsonrpc_error(
251 251 request,
252 252 retid=request.rpc_id,
253 253 message='This method [%s] does not support '
254 254 'required parameter `%s`' % (func.__name__, arg))
255 255
256 256 # get our arglist and check if we provided them as args
257 257 for arg, default in func_kwargs.items():
258 258 if arg in [user_var, request_var]:
259 259 # user_var and request_var are pre-hardcoded parameters and we
260 260 # don't need to do any translation
261 261 continue
262 262
263 263 # skip the required param check if it's default value is
264 264 # NotImplementedType (default_empty)
265 265 if default == default_empty and arg not in request.rpc_params:
266 266 return jsonrpc_error(
267 267 request,
268 268 retid=request.rpc_id,
269 269 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 270 )
271 271
272 272 # sanitize extra passed arguments
273 273 for k in request.rpc_params.keys()[:]:
274 274 if k not in func_kwargs:
275 275 del request.rpc_params[k]
276 276
277 277 call_params = request.rpc_params
278 278 call_params.update({
279 279 'request': request,
280 280 'apiuser': auth_u
281 281 })
282 282
283 283 # register some common functions for usage
284 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id,
285 attach_to_request=True)
284 attach_context_attributes(
285 TemplateArgs(), request, request.rpc_user.user_id)
286 286
287 287 try:
288 288 ret_value = func(**call_params)
289 289 return jsonrpc_response(request, ret_value)
290 290 except JSONRPCBaseError:
291 291 raise
292 292 except Exception:
293 293 log.exception('Unhandled exception occurred on api call: %s', func)
294 294 return jsonrpc_error(request, retid=request.rpc_id,
295 295 message='Internal server error')
296 296
297 297
298 298 def setup_request(request):
299 299 """
300 300 Parse a JSON-RPC request body. It's used inside the predicates method
301 301 to validate and bootstrap requests for usage in rpc calls.
302 302
303 303 We need to raise JSONRPCError here if we want to return some errors back to
304 304 user.
305 305 """
306 306
307 307 log.debug('Executing setup request: %r', request)
308 308 request.rpc_ip_addr = get_ip_addr(request.environ)
309 309 # TODO(marcink): deprecate GET at some point
310 310 if request.method not in ['POST', 'GET']:
311 311 log.debug('unsupported request method "%s"', request.method)
312 312 raise JSONRPCError(
313 313 'unsupported request method "%s". Please use POST' % request.method)
314 314
315 315 if 'CONTENT_LENGTH' not in request.environ:
316 316 log.debug("No Content-Length")
317 317 raise JSONRPCError("Empty body, No Content-Length in request")
318 318
319 319 else:
320 320 length = request.environ['CONTENT_LENGTH']
321 321 log.debug('Content-Length: %s', length)
322 322
323 323 if length == 0:
324 324 log.debug("Content-Length is 0")
325 325 raise JSONRPCError("Content-Length is 0")
326 326
327 327 raw_body = request.body
328 328 try:
329 329 json_body = json.loads(raw_body)
330 330 except ValueError as e:
331 331 # catch JSON errors Here
332 332 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333 333
334 334 request.rpc_id = json_body.get('id')
335 335 request.rpc_method = json_body.get('method')
336 336
337 337 # check required base parameters
338 338 try:
339 339 api_key = json_body.get('api_key')
340 340 if not api_key:
341 341 api_key = json_body.get('auth_token')
342 342
343 343 if not api_key:
344 344 raise KeyError('api_key or auth_token')
345 345
346 346 # TODO(marcink): support passing in token in request header
347 347
348 348 request.rpc_api_key = api_key
349 349 request.rpc_id = json_body['id']
350 350 request.rpc_method = json_body['method']
351 351 request.rpc_params = json_body['args'] \
352 352 if isinstance(json_body['args'], dict) else {}
353 353
354 354 log.debug(
355 355 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
356 356 except KeyError as e:
357 357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358 358
359 359 log.debug('setup complete, now handling method:%s rpcid:%s',
360 360 request.rpc_method, request.rpc_id, )
361 361
362 362
363 363 class RoutePredicate(object):
364 364 def __init__(self, val, config):
365 365 self.val = val
366 366
367 367 def text(self):
368 368 return 'jsonrpc route = %s' % self.val
369 369
370 370 phash = text
371 371
372 372 def __call__(self, info, request):
373 373 if self.val:
374 374 # potentially setup and bootstrap our call
375 375 setup_request(request)
376 376
377 377 # Always return True so that even if it isn't a valid RPC it
378 378 # will fall through to the underlaying handlers like notfound_view
379 379 return True
380 380
381 381
382 382 class NotFoundPredicate(object):
383 383 def __init__(self, val, config):
384 384 self.val = val
385 385 self.methods = config.registry.jsonrpc_methods
386 386
387 387 def text(self):
388 388 return 'jsonrpc method not found = {}.'.format(self.val)
389 389
390 390 phash = text
391 391
392 392 def __call__(self, info, request):
393 393 return hasattr(request, 'rpc_method')
394 394
395 395
396 396 class MethodPredicate(object):
397 397 def __init__(self, val, config):
398 398 self.method = val
399 399
400 400 def text(self):
401 401 return 'jsonrpc method = %s' % self.method
402 402
403 403 phash = text
404 404
405 405 def __call__(self, context, request):
406 406 # we need to explicitly return False here, so pyramid doesn't try to
407 407 # execute our view directly. We need our main handler to execute things
408 408 return getattr(request, 'rpc_method') == self.method
409 409
410 410
411 411 def add_jsonrpc_method(config, view, **kwargs):
412 412 # pop the method name
413 413 method = kwargs.pop('method', None)
414 414
415 415 if method is None:
416 416 raise ConfigurationError(
417 417 'Cannot register a JSON-RPC method without specifying the '
418 418 '"method"')
419 419
420 420 # we define custom predicate, to enable to detect conflicting methods,
421 421 # those predicates are kind of "translation" from the decorator variables
422 422 # to internal predicates names
423 423
424 424 kwargs['jsonrpc_method'] = method
425 425
426 426 # register our view into global view store for validation
427 427 config.registry.jsonrpc_methods[method] = view
428 428
429 429 # we're using our main request_view handler, here, so each method
430 430 # has a unified handler for itself
431 431 config.add_view(request_view, route_name='apiv2', **kwargs)
432 432
433 433
434 434 class jsonrpc_method(object):
435 435 """
436 436 decorator that works similar to @add_view_config decorator,
437 437 but tailored for our JSON RPC
438 438 """
439 439
440 440 venusian = venusian # for testing injection
441 441
442 442 def __init__(self, method=None, **kwargs):
443 443 self.method = method
444 444 self.kwargs = kwargs
445 445
446 446 def __call__(self, wrapped):
447 447 kwargs = self.kwargs.copy()
448 448 kwargs['method'] = self.method or wrapped.__name__
449 449 depth = kwargs.pop('_depth', 0)
450 450
451 451 def callback(context, name, ob):
452 452 config = context.config.with_package(info.module)
453 453 config.add_jsonrpc_method(view=ob, **kwargs)
454 454
455 455 info = venusian.attach(wrapped, callback, category='pyramid',
456 456 depth=depth + 1)
457 457 if info.scope == 'class':
458 458 # ensure that attr is set if decorating a class method
459 459 kwargs.setdefault('attr', wrapped.__name__)
460 460
461 461 kwargs['_info'] = info.codeinfo # fbo action_method
462 462 return wrapped
463 463
464 464
465 465 class jsonrpc_deprecated_method(object):
466 466 """
467 467 Marks method as deprecated, adds log.warning, and inject special key to
468 468 the request variable to mark method as deprecated.
469 469 Also injects special docstring that extract_docs will catch to mark
470 470 method as deprecated.
471 471
472 472 :param use_method: specify which method should be used instead of
473 473 the decorated one
474 474
475 475 Use like::
476 476
477 477 @jsonrpc_method()
478 478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 479 def old_func(request, apiuser, arg1, arg2):
480 480 ...
481 481 """
482 482
483 483 def __init__(self, use_method, deprecated_at_version):
484 484 self.use_method = use_method
485 485 self.deprecated_at_version = deprecated_at_version
486 486 self.deprecated_msg = ''
487 487
488 488 def __call__(self, func):
489 489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 490 method=self.use_method)
491 491
492 492 docstring = """\n
493 493 .. deprecated:: {version}
494 494
495 495 {deprecation_message}
496 496
497 497 {original_docstring}
498 498 """
499 499 func.__doc__ = docstring.format(
500 500 version=self.deprecated_at_version,
501 501 deprecation_message=self.deprecated_msg,
502 502 original_docstring=func.__doc__)
503 503 return decorator.decorator(self.__wrapper, func)
504 504
505 505 def __wrapper(self, func, *fargs, **fkwargs):
506 506 log.warning('DEPRECATED API CALL on function %s, please '
507 507 'use `%s` instead', func, self.use_method)
508 508 # alter function docstring to mark as deprecated, this is picked up
509 509 # via fabric file that generates API DOC.
510 510 result = func(*fargs, **fkwargs)
511 511
512 512 request = fargs[0]
513 513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 514 return result
515 515
516 516
517 517 def includeme(config):
518 518 plugin_module = 'rhodecode.api'
519 519 plugin_settings = get_plugin_settings(
520 520 plugin_module, config.registry.settings)
521 521
522 522 if not hasattr(config.registry, 'jsonrpc_methods'):
523 523 config.registry.jsonrpc_methods = OrderedDict()
524 524
525 525 # match filter by given method only
526 526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527 527
528 528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 529 serializer=json.dumps, indent=4))
530 530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531 531
532 532 config.add_route_predicate(
533 533 'jsonrpc_call', RoutePredicate)
534 534
535 535 config.add_route(
536 536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537 537
538 538 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 539 # register some exception handling view
540 540 config.add_view(exception_view, context=JSONRPCBaseError)
541 541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,605 +1,604 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31 import pyramid.threadlocal
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36 from pylons import config, tmpl_context as c, request, session, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(ip_addr)
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def get_user_agent(environ):
166 166 return environ.get('HTTP_USER_AGENT')
167 167
168 168
169 169 def vcs_operation_context(
170 170 environ, repo_name, username, action, scm, check_locking=True,
171 171 is_shadow_repo=False):
172 172 """
173 173 Generate the context for a vcs operation, e.g. push or pull.
174 174
175 175 This context is passed over the layers so that hooks triggered by the
176 176 vcs operation know details like the user, the user's IP address etc.
177 177
178 178 :param check_locking: Allows to switch of the computation of the locking
179 179 data. This serves mainly the need of the simplevcs middleware to be
180 180 able to disable this for certain operations.
181 181
182 182 """
183 183 # Tri-state value: False: unlock, None: nothing, True: lock
184 184 make_lock = None
185 185 locked_by = [None, None, None]
186 186 is_anonymous = username == User.DEFAULT_USER
187 187 if not is_anonymous and check_locking:
188 188 log.debug('Checking locking on repository "%s"', repo_name)
189 189 user = User.get_by_username(username)
190 190 repo = Repository.get_by_repo_name(repo_name)
191 191 make_lock, __, locked_by = repo.get_locking_state(
192 192 action, user.user_id)
193 193
194 194 settings_model = VcsSettingsModel(repo=repo_name)
195 195 ui_settings = settings_model.get_ui_settings()
196 196
197 197 extras = {
198 198 'ip': get_ip_addr(environ),
199 199 'username': username,
200 200 'action': action,
201 201 'repository': repo_name,
202 202 'scm': scm,
203 203 'config': rhodecode.CONFIG['__file__'],
204 204 'make_lock': make_lock,
205 205 'locked_by': locked_by,
206 206 'server_url': utils2.get_server_url(environ),
207 207 'user_agent': get_user_agent(environ),
208 208 'hooks': get_enabled_hook_classes(ui_settings),
209 209 'is_shadow_repo': is_shadow_repo,
210 210 }
211 211 return extras
212 212
213 213
214 214 class BasicAuth(AuthBasicAuthenticator):
215 215
216 216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 217 initial_call_detection=False, acl_repo_name=None):
218 218 self.realm = realm
219 219 self.initial_call = initial_call_detection
220 220 self.authfunc = authfunc
221 221 self.registry = registry
222 222 self.acl_repo_name = acl_repo_name
223 223 self._rc_auth_http_code = auth_http_code
224 224
225 225 def _get_response_from_code(self, http_code):
226 226 try:
227 227 return get_exception(safe_int(http_code))
228 228 except Exception:
229 229 log.exception('Failed to fetch response for code %s' % http_code)
230 230 return HTTPForbidden
231 231
232 232 def build_authentication(self):
233 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 234 if self._rc_auth_http_code and not self.initial_call:
235 235 # return alternative HTTP code if alternative http return code
236 236 # is specified in RhodeCode config, but ONLY if it's not the
237 237 # FIRST call
238 238 custom_response_klass = self._get_response_from_code(
239 239 self._rc_auth_http_code)
240 240 return custom_response_klass(headers=head)
241 241 return HTTPUnauthorized(headers=head)
242 242
243 243 def authenticate(self, environ):
244 244 authorization = AUTHORIZATION(environ)
245 245 if not authorization:
246 246 return self.build_authentication()
247 247 (authmeth, auth) = authorization.split(' ', 1)
248 248 if 'basic' != authmeth.lower():
249 249 return self.build_authentication()
250 250 auth = auth.strip().decode('base64')
251 251 _parts = auth.split(':', 1)
252 252 if len(_parts) == 2:
253 253 username, password = _parts
254 254 if self.authfunc(
255 255 username, password, environ, VCS_TYPE,
256 256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 257 return username
258 258 if username and password:
259 259 # we mark that we actually executed authentication once, at
260 260 # that point we can use the alternative auth code
261 261 self.initial_call = False
262 262
263 263 return self.build_authentication()
264 264
265 265 __call__ = authenticate
266 266
267 267
268 def attach_context_attributes(context, request, user_id, attach_to_request=False):
269 268 def calculate_version_hash():
270 269 return md5(
271 270 config.get('beaker.session.secret', '') +
272 271 rhodecode.__version__)[:8]
273 272
274 273
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 if attach_to_request:
401 request.call_context = context
402 else:
403 context.pyramid_request = pyramid.threadlocal.get_current_request()
400 context.pyramid_request = pyramid.threadlocal.get_current_request()
404 401
402 # attach the whole call context to the request
403 request.call_context = context
405 404
406 405
407 406 def get_auth_user(environ):
408 407 ip_addr = get_ip_addr(environ)
409 408 # make sure that we update permissions each time we call controller
410 409 _auth_token = (request.GET.get('auth_token', '') or
411 410 request.GET.get('api_key', ''))
412 411
413 412 if _auth_token:
414 413 # when using API_KEY we assume user exists, and
415 414 # doesn't need auth based on cookies.
416 415 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
417 416 authenticated = False
418 417 else:
419 418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
420 419 try:
421 420 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
422 421 ip_addr=ip_addr)
423 422 except UserCreationError as e:
424 423 h.flash(e, 'error')
425 424 # container auth or other auth functions that create users
426 425 # on the fly can throw this exception signaling that there's
427 426 # issue with user creation, explanation should be provided
428 427 # in Exception itself. We then create a simple blank
429 428 # AuthUser
430 429 auth_user = AuthUser(ip_addr=ip_addr)
431 430
432 431 if password_changed(auth_user, session):
433 432 session.invalidate()
434 433 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
435 434 auth_user = AuthUser(ip_addr=ip_addr)
436 435
437 436 authenticated = cookie_store.get('is_authenticated')
438 437
439 438 if not auth_user.is_authenticated and auth_user.is_user_object:
440 439 # user is not authenticated and not empty
441 440 auth_user.set_authenticated(authenticated)
442 441
443 442 return auth_user
444 443
445 444
446 445 class BaseController(WSGIController):
447 446
448 447 def __before__(self):
449 448 """
450 449 __before__ is called before controller methods and after __call__
451 450 """
452 451 # on each call propagate settings calls into global settings.
453 452 set_rhodecode_config(config)
454 453 attach_context_attributes(c, request, c.rhodecode_user.user_id)
455 454
456 455 # TODO: Remove this when fixed in attach_context_attributes()
457 456 c.repo_name = get_repo_slug(request) # can be empty
458 457
459 458 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
460 459 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
461 460 self.sa = meta.Session
462 461 self.scm_model = ScmModel(self.sa)
463 462
464 463 # set user language
465 464 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
466 465 if user_lang:
467 466 translation.set_lang(user_lang)
468 467 log.debug('set language to %s for user %s',
469 468 user_lang, self._rhodecode_user)
470 469
471 470 def _dispatch_redirect(self, with_url, environ, start_response):
472 471 resp = HTTPFound(with_url)
473 472 environ['SCRIPT_NAME'] = '' # handle prefix middleware
474 473 environ['PATH_INFO'] = with_url
475 474 return resp(environ, start_response)
476 475
477 476 def __call__(self, environ, start_response):
478 477 """Invoke the Controller"""
479 478 # WSGIController.__call__ dispatches to the Controller method
480 479 # the request is routed to. This routing information is
481 480 # available in environ['pylons.routes_dict']
482 481 from rhodecode.lib import helpers as h
483 482
484 483 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
485 484 if environ.get('debugtoolbar.wants_pylons_context', False):
486 485 environ['debugtoolbar.pylons_context'] = c._current_obj()
487 486
488 487 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
489 488 environ['pylons.routes_dict']['action']])
490 489
491 490 self.rc_config = SettingsModel().get_all_settings(cache=True)
492 491 self.ip_addr = get_ip_addr(environ)
493 492
494 493 # The rhodecode auth user is looked up and passed through the
495 494 # environ by the pylons compatibility tween in pyramid.
496 495 # So we can just grab it from there.
497 496 auth_user = environ['rc_auth_user']
498 497
499 498 # set globals for auth user
500 499 request.user = auth_user
501 500 c.rhodecode_user = self._rhodecode_user = auth_user
502 501
503 502 log.info('IP: %s User: %s accessed %s [%s]' % (
504 503 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
505 504 _route_name)
506 505 )
507 506
508 507 user_obj = auth_user.get_instance()
509 508 if user_obj and user_obj.user_data.get('force_password_change'):
510 509 h.flash('You are required to change your password', 'warning',
511 510 ignore_duplicate=True)
512 511 return self._dispatch_redirect(
513 512 url('my_account_password'), environ, start_response)
514 513
515 514 return WSGIController.__call__(self, environ, start_response)
516 515
517 516
518 517 class BaseRepoController(BaseController):
519 518 """
520 519 Base class for controllers responsible for loading all needed data for
521 520 repository loaded items are
522 521
523 522 c.rhodecode_repo: instance of scm repository
524 523 c.rhodecode_db_repo: instance of db
525 524 c.repository_requirements_missing: shows that repository specific data
526 525 could not be displayed due to the missing requirements
527 526 c.repository_pull_requests: show number of open pull requests
528 527 """
529 528
530 529 def __before__(self):
531 530 super(BaseRepoController, self).__before__()
532 531 if c.repo_name: # extracted from routes
533 532 db_repo = Repository.get_by_repo_name(c.repo_name)
534 533 if not db_repo:
535 534 return
536 535
537 536 log.debug(
538 537 'Found repository in database %s with state `%s`',
539 538 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
540 539 route = getattr(request.environ.get('routes.route'), 'name', '')
541 540
542 541 # allow to delete repos that are somehow damages in filesystem
543 542 if route in ['delete_repo']:
544 543 return
545 544
546 545 if db_repo.repo_state in [Repository.STATE_PENDING]:
547 546 if route in ['repo_creating_home']:
548 547 return
549 548 check_url = url('repo_creating_home', repo_name=c.repo_name)
550 549 return redirect(check_url)
551 550
552 551 self.rhodecode_db_repo = db_repo
553 552
554 553 missing_requirements = False
555 554 try:
556 555 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
557 556 except RepositoryRequirementError as e:
558 557 missing_requirements = True
559 558 self._handle_missing_requirements(e)
560 559
561 560 if self.rhodecode_repo is None and not missing_requirements:
562 561 log.error('%s this repository is present in database but it '
563 562 'cannot be created as an scm instance', c.repo_name)
564 563
565 564 h.flash(_(
566 565 "The repository at %(repo_name)s cannot be located.") %
567 566 {'repo_name': c.repo_name},
568 567 category='error', ignore_duplicate=True)
569 568 redirect(h.route_path('home'))
570 569
571 570 # update last change according to VCS data
572 571 if not missing_requirements:
573 572 commit = db_repo.get_commit(
574 573 pre_load=["author", "date", "message", "parents"])
575 574 db_repo.update_commit_cache(commit)
576 575
577 576 # Prepare context
578 577 c.rhodecode_db_repo = db_repo
579 578 c.rhodecode_repo = self.rhodecode_repo
580 579 c.repository_requirements_missing = missing_requirements
581 580
582 581 self._update_global_counters(self.scm_model, db_repo)
583 582
584 583 def _update_global_counters(self, scm_model, db_repo):
585 584 """
586 585 Base variables that are exposed to every page of repository
587 586 """
588 587 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
589 588
590 589 def _handle_missing_requirements(self, error):
591 590 self.rhodecode_repo = None
592 591 log.error(
593 592 'Requirements are missing for repository %s: %s',
594 593 c.repo_name, error.message)
595 594
596 595 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
597 596 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
598 597 settings_update_url = url('repo', repo_name=c.repo_name)
599 598 path = request.path
600 599 should_redirect = (
601 600 path not in (summary_url, settings_update_url)
602 601 and '/settings' not in path or path == statistics_url
603 602 )
604 603 if should_redirect:
605 604 redirect(summary_url)
General Comments 0
You need to be logged in to leave comments. Login now