##// END OF EJS Templates
api: attach the call context variables to request for later usage...
dan -
r1794:1c6b274b default
parent child Browse files
Show More
@@ -1,536 +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 from rhodecode.apps._base import TemplateArgs
38 39 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 41 from rhodecode.lib.ext_json import json
41 42 from rhodecode.lib.utils2 import safe_str
42 43 from rhodecode.lib.plugins.utils import get_plugin_settings
43 44 from rhodecode.model.db import User, UserApiKeys
44 45
45 46 log = logging.getLogger(__name__)
46 47
47 48 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 49 DEFAULT_URL = '/_admin/apiv2'
49 50
50 51
51 52 def find_methods(jsonrpc_methods, pattern):
52 53 matches = OrderedDict()
53 54 if not isinstance(pattern, (list, tuple)):
54 55 pattern = [pattern]
55 56
56 57 for single_pattern in pattern:
57 58 for method_name, method in jsonrpc_methods.items():
58 59 if fnmatch.fnmatch(method_name, single_pattern):
59 60 matches[method_name] = method
60 61 return matches
61 62
62 63
63 64 class ExtJsonRenderer(object):
64 65 """
65 66 Custom renderer that mkaes use of our ext_json lib
66 67
67 68 """
68 69
69 70 def __init__(self, serializer=json.dumps, **kw):
70 71 """ Any keyword arguments will be passed to the ``serializer``
71 72 function."""
72 73 self.serializer = serializer
73 74 self.kw = kw
74 75
75 76 def __call__(self, info):
76 77 """ Returns a plain JSON-encoded string with content-type
77 78 ``application/json``. The content-type may be overridden by
78 79 setting ``request.response.content_type``."""
79 80
80 81 def _render(value, system):
81 82 request = system.get('request')
82 83 if request is not None:
83 84 response = request.response
84 85 ct = response.content_type
85 86 if ct == response.default_content_type:
86 87 response.content_type = 'application/json'
87 88
88 89 return self.serializer(value, **self.kw)
89 90
90 91 return _render
91 92
92 93
93 94 def jsonrpc_response(request, result):
94 95 rpc_id = getattr(request, 'rpc_id', None)
95 96 response = request.response
96 97
97 98 # store content_type before render is called
98 99 ct = response.content_type
99 100
100 101 ret_value = ''
101 102 if rpc_id:
102 103 ret_value = {
103 104 'id': rpc_id,
104 105 'result': result,
105 106 'error': None,
106 107 }
107 108
108 109 # fetch deprecation warnings, and store it inside results
109 110 deprecation = getattr(request, 'rpc_deprecation', None)
110 111 if deprecation:
111 112 ret_value['DEPRECATION_WARNING'] = deprecation
112 113
113 114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
114 115 response.body = safe_str(raw_body, response.charset)
115 116
116 117 if ct == response.default_content_type:
117 118 response.content_type = 'application/json'
118 119
119 120 return response
120 121
121 122
122 123 def jsonrpc_error(request, message, retid=None, code=None):
123 124 """
124 125 Generate a Response object with a JSON-RPC error body
125 126
126 127 :param code:
127 128 :param retid:
128 129 :param message:
129 130 """
130 131 err_dict = {'id': retid, 'result': None, 'error': message}
131 132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
132 133 return Response(
133 134 body=body,
134 135 status=code,
135 136 content_type='application/json'
136 137 )
137 138
138 139
139 140 def exception_view(exc, request):
140 141 rpc_id = getattr(request, 'rpc_id', None)
141 142
142 143 fault_message = 'undefined error'
143 144 if isinstance(exc, JSONRPCError):
144 145 fault_message = exc.message
145 146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
146 147 elif isinstance(exc, JSONRPCValidationError):
147 148 colander_exc = exc.colander_exception
148 149 # TODO(marcink): think maybe of nicer way to serialize errors ?
149 150 fault_message = colander_exc.asdict()
150 151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
151 152 elif isinstance(exc, JSONRPCForbidden):
152 153 fault_message = 'Access was denied to this resource.'
153 154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
154 155 elif isinstance(exc, HTTPNotFound):
155 156 method = request.rpc_method
156 157 log.debug('json-rpc method `%s` not found in list of '
157 158 'api calls: %s, rpc_id:%s',
158 159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
159 160
160 161 similar = 'none'
161 162 try:
162 163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
163 164 similar_found = find_methods(
164 165 request.registry.jsonrpc_methods, similar_paterns)
165 166 similar = ', '.join(similar_found.keys()) or similar
166 167 except Exception:
167 168 # make the whole above block safe
168 169 pass
169 170
170 171 fault_message = "No such method: {}. Similar methods: {}".format(
171 172 method, similar)
172 173
173 174 return jsonrpc_error(request, fault_message, rpc_id)
174 175
175 176
176 177 def request_view(request):
177 178 """
178 179 Main request handling method. It handles all logic to call a specific
179 180 exposed method
180 181 """
181 182
182 183 # check if we can find this session using api_key, get_by_auth_token
183 184 # search not expired tokens only
184 185
185 186 try:
186 187 api_user = User.get_by_auth_token(request.rpc_api_key)
187 188
188 189 if api_user is None:
189 190 return jsonrpc_error(
190 191 request, retid=request.rpc_id, message='Invalid API KEY')
191 192
192 193 if not api_user.active:
193 194 return jsonrpc_error(
194 195 request, retid=request.rpc_id,
195 196 message='Request from this user not allowed')
196 197
197 198 # check if we are allowed to use this IP
198 199 auth_u = AuthUser(
199 200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
200 201 if not auth_u.ip_allowed:
201 202 return jsonrpc_error(
202 203 request, retid=request.rpc_id,
203 204 message='Request from IP:%s not allowed' % (
204 205 request.rpc_ip_addr,))
205 206 else:
206 207 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
207 208
208 209 # register our auth-user
209 210 request.rpc_user = auth_u
210 211
211 212 # now check if token is valid for API
212 213 auth_token = request.rpc_api_key
213 214 token_match = api_user.authenticate_by_token(
214 215 auth_token, roles=[UserApiKeys.ROLE_API])
215 216 invalid_token = not token_match
216 217
217 218 log.debug('Checking if API KEY is valid with proper role')
218 219 if invalid_token:
219 220 return jsonrpc_error(
220 221 request, retid=request.rpc_id,
221 222 message='API KEY invalid or, has bad role for an API call')
222 223
223 224 except Exception:
224 225 log.exception('Error on API AUTH')
225 226 return jsonrpc_error(
226 227 request, retid=request.rpc_id, message='Invalid API KEY')
227 228
228 229 method = request.rpc_method
229 230 func = request.registry.jsonrpc_methods[method]
230 231
231 232 # now that we have a method, add request._req_params to
232 233 # self.kargs and dispatch control to WGIController
233 234 argspec = inspect.getargspec(func)
234 235 arglist = argspec[0]
235 236 defaults = map(type, argspec[3] or [])
236 237 default_empty = types.NotImplementedType
237 238
238 239 # kw arguments required by this method
239 240 func_kwargs = dict(itertools.izip_longest(
240 241 reversed(arglist), reversed(defaults), fillvalue=default_empty))
241 242
242 243 # This attribute will need to be first param of a method that uses
243 244 # api_key, which is translated to instance of user at that name
244 245 user_var = 'apiuser'
245 246 request_var = 'request'
246 247
247 248 for arg in [user_var, request_var]:
248 249 if arg not in arglist:
249 250 return jsonrpc_error(
250 251 request,
251 252 retid=request.rpc_id,
252 253 message='This method [%s] does not support '
253 254 'required parameter `%s`' % (func.__name__, arg))
254 255
255 256 # get our arglist and check if we provided them as args
256 257 for arg, default in func_kwargs.items():
257 258 if arg in [user_var, request_var]:
258 259 # user_var and request_var are pre-hardcoded parameters and we
259 260 # don't need to do any translation
260 261 continue
261 262
262 263 # skip the required param check if it's default value is
263 264 # NotImplementedType (default_empty)
264 265 if default == default_empty and arg not in request.rpc_params:
265 266 return jsonrpc_error(
266 267 request,
267 268 retid=request.rpc_id,
268 269 message=('Missing non optional `%s` arg in JSON DATA' % arg)
269 270 )
270 271
271 272 # sanitize extra passed arguments
272 273 for k in request.rpc_params.keys()[:]:
273 274 if k not in func_kwargs:
274 275 del request.rpc_params[k]
275 276
276 277 call_params = request.rpc_params
277 278 call_params.update({
278 279 'request': request,
279 280 'apiuser': auth_u
280 281 })
282
283 # register some common functions for usage
284 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id,
285 attach_to_request=True)
286
281 287 try:
282 288 ret_value = func(**call_params)
283 289 return jsonrpc_response(request, ret_value)
284 290 except JSONRPCBaseError:
285 291 raise
286 292 except Exception:
287 293 log.exception('Unhandled exception occurred on api call: %s', func)
288 294 return jsonrpc_error(request, retid=request.rpc_id,
289 295 message='Internal server error')
290 296
291 297
292 298 def setup_request(request):
293 299 """
294 300 Parse a JSON-RPC request body. It's used inside the predicates method
295 301 to validate and bootstrap requests for usage in rpc calls.
296 302
297 303 We need to raise JSONRPCError here if we want to return some errors back to
298 304 user.
299 305 """
300 306
301 307 log.debug('Executing setup request: %r', request)
302 308 request.rpc_ip_addr = get_ip_addr(request.environ)
303 309 # TODO(marcink): deprecate GET at some point
304 310 if request.method not in ['POST', 'GET']:
305 311 log.debug('unsupported request method "%s"', request.method)
306 312 raise JSONRPCError(
307 313 'unsupported request method "%s". Please use POST' % request.method)
308 314
309 315 if 'CONTENT_LENGTH' not in request.environ:
310 316 log.debug("No Content-Length")
311 317 raise JSONRPCError("Empty body, No Content-Length in request")
312 318
313 319 else:
314 320 length = request.environ['CONTENT_LENGTH']
315 321 log.debug('Content-Length: %s', length)
316 322
317 323 if length == 0:
318 324 log.debug("Content-Length is 0")
319 325 raise JSONRPCError("Content-Length is 0")
320 326
321 327 raw_body = request.body
322 328 try:
323 329 json_body = json.loads(raw_body)
324 330 except ValueError as e:
325 331 # catch JSON errors Here
326 332 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
327 333
328 334 request.rpc_id = json_body.get('id')
329 335 request.rpc_method = json_body.get('method')
330 336
331 337 # check required base parameters
332 338 try:
333 339 api_key = json_body.get('api_key')
334 340 if not api_key:
335 341 api_key = json_body.get('auth_token')
336 342
337 343 if not api_key:
338 344 raise KeyError('api_key or auth_token')
339 345
340 346 # TODO(marcink): support passing in token in request header
341 347
342 348 request.rpc_api_key = api_key
343 349 request.rpc_id = json_body['id']
344 350 request.rpc_method = json_body['method']
345 351 request.rpc_params = json_body['args'] \
346 352 if isinstance(json_body['args'], dict) else {}
347 353
348 354 log.debug(
349 355 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
350 356 except KeyError as e:
351 357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
352 358
353 359 log.debug('setup complete, now handling method:%s rpcid:%s',
354 360 request.rpc_method, request.rpc_id, )
355 361
356 362
357 363 class RoutePredicate(object):
358 364 def __init__(self, val, config):
359 365 self.val = val
360 366
361 367 def text(self):
362 368 return 'jsonrpc route = %s' % self.val
363 369
364 370 phash = text
365 371
366 372 def __call__(self, info, request):
367 373 if self.val:
368 374 # potentially setup and bootstrap our call
369 375 setup_request(request)
370 376
371 377 # Always return True so that even if it isn't a valid RPC it
372 378 # will fall through to the underlaying handlers like notfound_view
373 379 return True
374 380
375 381
376 382 class NotFoundPredicate(object):
377 383 def __init__(self, val, config):
378 384 self.val = val
379 385 self.methods = config.registry.jsonrpc_methods
380 386
381 387 def text(self):
382 388 return 'jsonrpc method not found = {}.'.format(self.val)
383 389
384 390 phash = text
385 391
386 392 def __call__(self, info, request):
387 393 return hasattr(request, 'rpc_method')
388 394
389 395
390 396 class MethodPredicate(object):
391 397 def __init__(self, val, config):
392 398 self.method = val
393 399
394 400 def text(self):
395 401 return 'jsonrpc method = %s' % self.method
396 402
397 403 phash = text
398 404
399 405 def __call__(self, context, request):
400 406 # we need to explicitly return False here, so pyramid doesn't try to
401 407 # execute our view directly. We need our main handler to execute things
402 408 return getattr(request, 'rpc_method') == self.method
403 409
404 410
405 411 def add_jsonrpc_method(config, view, **kwargs):
406 412 # pop the method name
407 413 method = kwargs.pop('method', None)
408 414
409 415 if method is None:
410 416 raise ConfigurationError(
411 417 'Cannot register a JSON-RPC method without specifying the '
412 418 '"method"')
413 419
414 420 # we define custom predicate, to enable to detect conflicting methods,
415 421 # those predicates are kind of "translation" from the decorator variables
416 422 # to internal predicates names
417 423
418 424 kwargs['jsonrpc_method'] = method
419 425
420 426 # register our view into global view store for validation
421 427 config.registry.jsonrpc_methods[method] = view
422 428
423 429 # we're using our main request_view handler, here, so each method
424 430 # has a unified handler for itself
425 431 config.add_view(request_view, route_name='apiv2', **kwargs)
426 432
427 433
428 434 class jsonrpc_method(object):
429 435 """
430 436 decorator that works similar to @add_view_config decorator,
431 437 but tailored for our JSON RPC
432 438 """
433 439
434 440 venusian = venusian # for testing injection
435 441
436 442 def __init__(self, method=None, **kwargs):
437 443 self.method = method
438 444 self.kwargs = kwargs
439 445
440 446 def __call__(self, wrapped):
441 447 kwargs = self.kwargs.copy()
442 448 kwargs['method'] = self.method or wrapped.__name__
443 449 depth = kwargs.pop('_depth', 0)
444 450
445 451 def callback(context, name, ob):
446 452 config = context.config.with_package(info.module)
447 453 config.add_jsonrpc_method(view=ob, **kwargs)
448 454
449 455 info = venusian.attach(wrapped, callback, category='pyramid',
450 456 depth=depth + 1)
451 457 if info.scope == 'class':
452 458 # ensure that attr is set if decorating a class method
453 459 kwargs.setdefault('attr', wrapped.__name__)
454 460
455 461 kwargs['_info'] = info.codeinfo # fbo action_method
456 462 return wrapped
457 463
458 464
459 465 class jsonrpc_deprecated_method(object):
460 466 """
461 467 Marks method as deprecated, adds log.warning, and inject special key to
462 468 the request variable to mark method as deprecated.
463 469 Also injects special docstring that extract_docs will catch to mark
464 470 method as deprecated.
465 471
466 472 :param use_method: specify which method should be used instead of
467 473 the decorated one
468 474
469 475 Use like::
470 476
471 477 @jsonrpc_method()
472 478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
473 479 def old_func(request, apiuser, arg1, arg2):
474 480 ...
475 481 """
476 482
477 483 def __init__(self, use_method, deprecated_at_version):
478 484 self.use_method = use_method
479 485 self.deprecated_at_version = deprecated_at_version
480 486 self.deprecated_msg = ''
481 487
482 488 def __call__(self, func):
483 489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
484 490 method=self.use_method)
485 491
486 492 docstring = """\n
487 493 .. deprecated:: {version}
488 494
489 495 {deprecation_message}
490 496
491 497 {original_docstring}
492 498 """
493 499 func.__doc__ = docstring.format(
494 500 version=self.deprecated_at_version,
495 501 deprecation_message=self.deprecated_msg,
496 502 original_docstring=func.__doc__)
497 503 return decorator.decorator(self.__wrapper, func)
498 504
499 505 def __wrapper(self, func, *fargs, **fkwargs):
500 506 log.warning('DEPRECATED API CALL on function %s, please '
501 507 'use `%s` instead', func, self.use_method)
502 508 # alter function docstring to mark as deprecated, this is picked up
503 509 # via fabric file that generates API DOC.
504 510 result = func(*fargs, **fkwargs)
505 511
506 512 request = fargs[0]
507 513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
508 514 return result
509 515
510 516
511 517 def includeme(config):
512 518 plugin_module = 'rhodecode.api'
513 519 plugin_settings = get_plugin_settings(
514 520 plugin_module, config.registry.settings)
515 521
516 522 if not hasattr(config.registry, 'jsonrpc_methods'):
517 523 config.registry.jsonrpc_methods = OrderedDict()
518 524
519 525 # match filter by given method only
520 526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
521 527
522 528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
523 529 serializer=json.dumps, indent=4))
524 530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
525 531
526 532 config.add_route_predicate(
527 533 'jsonrpc_call', RoutePredicate)
528 534
529 535 config.add_route(
530 536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
531 537
532 538 config.scan(plugin_module, ignore='rhodecode.api.tests')
533 539 # register some exception handling view
534 540 config.add_view(exception_view, context=JSONRPCBaseError)
535 541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
536 542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,592 +1,596 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):
268 def attach_context_attributes(context, request, user_id, attach_to_request=False):
269 269 """
270 270 Attach variables into template context called `c`, please note that
271 271 request could be pylons or pyramid request in here.
272 272 """
273 273 rc_config = SettingsModel().get_all_settings(cache=True)
274 274
275 275 context.rhodecode_version = rhodecode.__version__
276 276 context.rhodecode_edition = config.get('rhodecode.edition')
277 277 # unique secret + version does not leak the version but keep consistency
278 278 context.rhodecode_version_hash = md5(
279 279 config.get('beaker.session.secret', '') +
280 280 rhodecode.__version__)[:8]
281 281
282 282 # Default language set for the incoming request
283 283 context.language = translation.get_lang()[0]
284 284
285 285 # Visual options
286 286 context.visual = AttributeDict({})
287 287
288 288 # DB stored Visual Items
289 289 context.visual.show_public_icon = str2bool(
290 290 rc_config.get('rhodecode_show_public_icon'))
291 291 context.visual.show_private_icon = str2bool(
292 292 rc_config.get('rhodecode_show_private_icon'))
293 293 context.visual.stylify_metatags = str2bool(
294 294 rc_config.get('rhodecode_stylify_metatags'))
295 295 context.visual.dashboard_items = safe_int(
296 296 rc_config.get('rhodecode_dashboard_items', 100))
297 297 context.visual.admin_grid_items = safe_int(
298 298 rc_config.get('rhodecode_admin_grid_items', 100))
299 299 context.visual.repository_fields = str2bool(
300 300 rc_config.get('rhodecode_repository_fields'))
301 301 context.visual.show_version = str2bool(
302 302 rc_config.get('rhodecode_show_version'))
303 303 context.visual.use_gravatar = str2bool(
304 304 rc_config.get('rhodecode_use_gravatar'))
305 305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
306 306 context.visual.default_renderer = rc_config.get(
307 307 'rhodecode_markup_renderer', 'rst')
308 308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
309 309 context.visual.rhodecode_support_url = \
310 310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
311 311
312 312 context.pre_code = rc_config.get('rhodecode_pre_code')
313 313 context.post_code = rc_config.get('rhodecode_post_code')
314 314 context.rhodecode_name = rc_config.get('rhodecode_title')
315 315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
316 316 # if we have specified default_encoding in the request, it has more
317 317 # priority
318 318 if request.GET.get('default_encoding'):
319 319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
320 320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
321 321
322 322 # INI stored
323 323 context.labs_active = str2bool(
324 324 config.get('labs_settings_active', 'false'))
325 325 context.visual.allow_repo_location_change = str2bool(
326 326 config.get('allow_repo_location_change', True))
327 327 context.visual.allow_custom_hooks_settings = str2bool(
328 328 config.get('allow_custom_hooks_settings', True))
329 329 context.debug_style = str2bool(config.get('debug_style', False))
330 330
331 331 context.rhodecode_instanceid = config.get('instance_id')
332 332
333 333 # AppEnlight
334 334 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
335 335 context.appenlight_api_public_key = config.get(
336 336 'appenlight.api_public_key', '')
337 337 context.appenlight_server_url = config.get('appenlight.server_url', '')
338 338
339 339 # JS template context
340 340 context.template_context = {
341 341 'repo_name': None,
342 342 'repo_type': None,
343 343 'repo_landing_commit': None,
344 344 'rhodecode_user': {
345 345 'username': None,
346 346 'email': None,
347 347 'notification_status': False
348 348 },
349 349 'visual': {
350 350 'default_renderer': None
351 351 },
352 352 'commit_data': {
353 353 'commit_id': None
354 354 },
355 355 'pull_request_data': {'pull_request_id': None},
356 356 'timeago': {
357 357 'refresh_time': 120 * 1000,
358 358 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
359 359 },
360 360 'pylons_dispatch': {
361 361 # 'controller': request.environ['pylons.routes_dict']['controller'],
362 362 # 'action': request.environ['pylons.routes_dict']['action'],
363 363 },
364 364 'pyramid_dispatch': {
365 365
366 366 },
367 367 'extra': {'plugins': {}}
368 368 }
369 369 # END CONFIG VARS
370 370
371 371 # TODO: This dosn't work when called from pylons compatibility tween.
372 372 # Fix this and remove it from base controller.
373 373 # context.repo_name = get_repo_slug(request) # can be empty
374 374
375 375 diffmode = 'sideside'
376 376 if request.GET.get('diffmode'):
377 377 if request.GET['diffmode'] == 'unified':
378 378 diffmode = 'unified'
379 379 elif request.session.get('diffmode'):
380 380 diffmode = request.session['diffmode']
381 381
382 382 context.diffmode = diffmode
383 383
384 384 if request.session.get('diffmode') != diffmode:
385 385 request.session['diffmode'] = diffmode
386 386
387 387 context.csrf_token = auth.get_csrf_token()
388 388 context.backends = rhodecode.BACKENDS.keys()
389 389 context.backends.sort()
390 390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
391 context.pyramid_request = pyramid.threadlocal.get_current_request()
391 if attach_to_request:
392 request.call_context = context
393 else:
394 context.pyramid_request = pyramid.threadlocal.get_current_request()
395
392 396
393 397
394 398 def get_auth_user(environ):
395 399 ip_addr = get_ip_addr(environ)
396 400 # make sure that we update permissions each time we call controller
397 401 _auth_token = (request.GET.get('auth_token', '') or
398 402 request.GET.get('api_key', ''))
399 403
400 404 if _auth_token:
401 405 # when using API_KEY we assume user exists, and
402 406 # doesn't need auth based on cookies.
403 407 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
404 408 authenticated = False
405 409 else:
406 410 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
407 411 try:
408 412 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
409 413 ip_addr=ip_addr)
410 414 except UserCreationError as e:
411 415 h.flash(e, 'error')
412 416 # container auth or other auth functions that create users
413 417 # on the fly can throw this exception signaling that there's
414 418 # issue with user creation, explanation should be provided
415 419 # in Exception itself. We then create a simple blank
416 420 # AuthUser
417 421 auth_user = AuthUser(ip_addr=ip_addr)
418 422
419 423 if password_changed(auth_user, session):
420 424 session.invalidate()
421 425 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
422 426 auth_user = AuthUser(ip_addr=ip_addr)
423 427
424 428 authenticated = cookie_store.get('is_authenticated')
425 429
426 430 if not auth_user.is_authenticated and auth_user.is_user_object:
427 431 # user is not authenticated and not empty
428 432 auth_user.set_authenticated(authenticated)
429 433
430 434 return auth_user
431 435
432 436
433 437 class BaseController(WSGIController):
434 438
435 439 def __before__(self):
436 440 """
437 441 __before__ is called before controller methods and after __call__
438 442 """
439 443 # on each call propagate settings calls into global settings.
440 444 set_rhodecode_config(config)
441 445 attach_context_attributes(c, request, c.rhodecode_user.user_id)
442 446
443 447 # TODO: Remove this when fixed in attach_context_attributes()
444 448 c.repo_name = get_repo_slug(request) # can be empty
445 449
446 450 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
447 451 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
448 452 self.sa = meta.Session
449 453 self.scm_model = ScmModel(self.sa)
450 454
451 455 # set user language
452 456 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
453 457 if user_lang:
454 458 translation.set_lang(user_lang)
455 459 log.debug('set language to %s for user %s',
456 460 user_lang, self._rhodecode_user)
457 461
458 462 def _dispatch_redirect(self, with_url, environ, start_response):
459 463 resp = HTTPFound(with_url)
460 464 environ['SCRIPT_NAME'] = '' # handle prefix middleware
461 465 environ['PATH_INFO'] = with_url
462 466 return resp(environ, start_response)
463 467
464 468 def __call__(self, environ, start_response):
465 469 """Invoke the Controller"""
466 470 # WSGIController.__call__ dispatches to the Controller method
467 471 # the request is routed to. This routing information is
468 472 # available in environ['pylons.routes_dict']
469 473 from rhodecode.lib import helpers as h
470 474
471 475 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
472 476 if environ.get('debugtoolbar.wants_pylons_context', False):
473 477 environ['debugtoolbar.pylons_context'] = c._current_obj()
474 478
475 479 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
476 480 environ['pylons.routes_dict']['action']])
477 481
478 482 self.rc_config = SettingsModel().get_all_settings(cache=True)
479 483 self.ip_addr = get_ip_addr(environ)
480 484
481 485 # The rhodecode auth user is looked up and passed through the
482 486 # environ by the pylons compatibility tween in pyramid.
483 487 # So we can just grab it from there.
484 488 auth_user = environ['rc_auth_user']
485 489
486 490 # set globals for auth user
487 491 request.user = auth_user
488 492 c.rhodecode_user = self._rhodecode_user = auth_user
489 493
490 494 log.info('IP: %s User: %s accessed %s [%s]' % (
491 495 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
492 496 _route_name)
493 497 )
494 498
495 499 user_obj = auth_user.get_instance()
496 500 if user_obj and user_obj.user_data.get('force_password_change'):
497 501 h.flash('You are required to change your password', 'warning',
498 502 ignore_duplicate=True)
499 503 return self._dispatch_redirect(
500 504 url('my_account_password'), environ, start_response)
501 505
502 506 return WSGIController.__call__(self, environ, start_response)
503 507
504 508
505 509 class BaseRepoController(BaseController):
506 510 """
507 511 Base class for controllers responsible for loading all needed data for
508 512 repository loaded items are
509 513
510 514 c.rhodecode_repo: instance of scm repository
511 515 c.rhodecode_db_repo: instance of db
512 516 c.repository_requirements_missing: shows that repository specific data
513 517 could not be displayed due to the missing requirements
514 518 c.repository_pull_requests: show number of open pull requests
515 519 """
516 520
517 521 def __before__(self):
518 522 super(BaseRepoController, self).__before__()
519 523 if c.repo_name: # extracted from routes
520 524 db_repo = Repository.get_by_repo_name(c.repo_name)
521 525 if not db_repo:
522 526 return
523 527
524 528 log.debug(
525 529 'Found repository in database %s with state `%s`',
526 530 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
527 531 route = getattr(request.environ.get('routes.route'), 'name', '')
528 532
529 533 # allow to delete repos that are somehow damages in filesystem
530 534 if route in ['delete_repo']:
531 535 return
532 536
533 537 if db_repo.repo_state in [Repository.STATE_PENDING]:
534 538 if route in ['repo_creating_home']:
535 539 return
536 540 check_url = url('repo_creating_home', repo_name=c.repo_name)
537 541 return redirect(check_url)
538 542
539 543 self.rhodecode_db_repo = db_repo
540 544
541 545 missing_requirements = False
542 546 try:
543 547 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
544 548 except RepositoryRequirementError as e:
545 549 missing_requirements = True
546 550 self._handle_missing_requirements(e)
547 551
548 552 if self.rhodecode_repo is None and not missing_requirements:
549 553 log.error('%s this repository is present in database but it '
550 554 'cannot be created as an scm instance', c.repo_name)
551 555
552 556 h.flash(_(
553 557 "The repository at %(repo_name)s cannot be located.") %
554 558 {'repo_name': c.repo_name},
555 559 category='error', ignore_duplicate=True)
556 560 redirect(h.route_path('home'))
557 561
558 562 # update last change according to VCS data
559 563 if not missing_requirements:
560 564 commit = db_repo.get_commit(
561 565 pre_load=["author", "date", "message", "parents"])
562 566 db_repo.update_commit_cache(commit)
563 567
564 568 # Prepare context
565 569 c.rhodecode_db_repo = db_repo
566 570 c.rhodecode_repo = self.rhodecode_repo
567 571 c.repository_requirements_missing = missing_requirements
568 572
569 573 self._update_global_counters(self.scm_model, db_repo)
570 574
571 575 def _update_global_counters(self, scm_model, db_repo):
572 576 """
573 577 Base variables that are exposed to every page of repository
574 578 """
575 579 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
576 580
577 581 def _handle_missing_requirements(self, error):
578 582 self.rhodecode_repo = None
579 583 log.error(
580 584 'Requirements are missing for repository %s: %s',
581 585 c.repo_name, error.message)
582 586
583 587 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
584 588 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
585 589 settings_update_url = url('repo', repo_name=c.repo_name)
586 590 path = request.path
587 591 should_redirect = (
588 592 path not in (summary_url, settings_update_url)
589 593 and '/settings' not in path or path == statistics_url
590 594 )
591 595 if should_redirect:
592 596 redirect(summary_url)
General Comments 0
You need to be logged in to leave comments. Login now