##// END OF EJS Templates
sessions: don't touch session for API calls.
marcink -
r3749:7da1bd06 new-ui
parent child Browse files
Show More
@@ -1,583 +1,592 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 markupsafe
31 31 import ipaddress
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
37 37 import rhodecode
38 38 from rhodecode.apps._base import TemplateArgs
39 39 from rhodecode.authentication.base import VCS_TYPE
40 40 from rhodecode.lib import auth, utils2
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 43 from rhodecode.lib.exceptions import UserCreationError
44 44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
45 45 from rhodecode.lib.utils2 import (
46 46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
47 47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
48 48 from rhodecode.model.notification import NotificationModel
49 49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def _filter_proxy(ip):
55 55 """
56 56 Passed in IP addresses in HEADERS can be in a special format of multiple
57 57 ips. Those comma separated IPs are passed from various proxies in the
58 58 chain of request processing. The left-most being the original client.
59 59 We only care about the first IP which came from the org. client.
60 60
61 61 :param ip: ip string from headers
62 62 """
63 63 if ',' in ip:
64 64 _ips = ip.split(',')
65 65 _first_ip = _ips[0].strip()
66 66 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
67 67 return _first_ip
68 68 return ip
69 69
70 70
71 71 def _filter_port(ip):
72 72 """
73 73 Removes a port from ip, there are 4 main cases to handle here.
74 74 - ipv4 eg. 127.0.0.1
75 75 - ipv6 eg. ::1
76 76 - ipv4+port eg. 127.0.0.1:8080
77 77 - ipv6+port eg. [::1]:8080
78 78
79 79 :param ip:
80 80 """
81 81 def is_ipv6(ip_addr):
82 82 if hasattr(socket, 'inet_pton'):
83 83 try:
84 84 socket.inet_pton(socket.AF_INET6, ip_addr)
85 85 except socket.error:
86 86 return False
87 87 else:
88 88 # fallback to ipaddress
89 89 try:
90 90 ipaddress.IPv6Address(safe_unicode(ip_addr))
91 91 except Exception:
92 92 return False
93 93 return True
94 94
95 95 if ':' not in ip: # must be ipv4 pure ip
96 96 return ip
97 97
98 98 if '[' in ip and ']' in ip: # ipv6 with port
99 99 return ip.split(']')[0][1:].lower()
100 100
101 101 # must be ipv6 or ipv4 with port
102 102 if is_ipv6(ip):
103 103 return ip
104 104 else:
105 105 ip, _port = ip.split(':')[:2] # means ipv4+port
106 106 return ip
107 107
108 108
109 109 def get_ip_addr(environ):
110 110 proxy_key = 'HTTP_X_REAL_IP'
111 111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
112 112 def_key = 'REMOTE_ADDR'
113 113 _filters = lambda x: _filter_port(_filter_proxy(x))
114 114
115 115 ip = environ.get(proxy_key)
116 116 if ip:
117 117 return _filters(ip)
118 118
119 119 ip = environ.get(proxy_key2)
120 120 if ip:
121 121 return _filters(ip)
122 122
123 123 ip = environ.get(def_key, '0.0.0.0')
124 124 return _filters(ip)
125 125
126 126
127 127 def get_server_ip_addr(environ, log_errors=True):
128 128 hostname = environ.get('SERVER_NAME')
129 129 try:
130 130 return socket.gethostbyname(hostname)
131 131 except Exception as e:
132 132 if log_errors:
133 133 # in some cases this lookup is not possible, and we don't want to
134 134 # make it an exception in logs
135 135 log.exception('Could not retrieve server ip address: %s', e)
136 136 return hostname
137 137
138 138
139 139 def get_server_port(environ):
140 140 return environ.get('SERVER_PORT')
141 141
142 142
143 143 def get_access_path(environ):
144 144 path = environ.get('PATH_INFO')
145 145 org_req = environ.get('pylons.original_request')
146 146 if org_req:
147 147 path = org_req.environ.get('PATH_INFO')
148 148 return path
149 149
150 150
151 151 def get_user_agent(environ):
152 152 return environ.get('HTTP_USER_AGENT')
153 153
154 154
155 155 def vcs_operation_context(
156 156 environ, repo_name, username, action, scm, check_locking=True,
157 157 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
158 158 """
159 159 Generate the context for a vcs operation, e.g. push or pull.
160 160
161 161 This context is passed over the layers so that hooks triggered by the
162 162 vcs operation know details like the user, the user's IP address etc.
163 163
164 164 :param check_locking: Allows to switch of the computation of the locking
165 165 data. This serves mainly the need of the simplevcs middleware to be
166 166 able to disable this for certain operations.
167 167
168 168 """
169 169 # Tri-state value: False: unlock, None: nothing, True: lock
170 170 make_lock = None
171 171 locked_by = [None, None, None]
172 172 is_anonymous = username == User.DEFAULT_USER
173 173 user = User.get_by_username(username)
174 174 if not is_anonymous and check_locking:
175 175 log.debug('Checking locking on repository "%s"', repo_name)
176 176 repo = Repository.get_by_repo_name(repo_name)
177 177 make_lock, __, locked_by = repo.get_locking_state(
178 178 action, user.user_id)
179 179 user_id = user.user_id
180 180 settings_model = VcsSettingsModel(repo=repo_name)
181 181 ui_settings = settings_model.get_ui_settings()
182 182
183 183 # NOTE(marcink): This should be also in sync with
184 184 # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data
185 185 store = [x for x in ui_settings if x.key == '/']
186 186 repo_store = ''
187 187 if store:
188 188 repo_store = store[0].value
189 189
190 190 scm_data = {
191 191 'ip': get_ip_addr(environ),
192 192 'username': username,
193 193 'user_id': user_id,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'repo_store': repo_store,
199 199 'make_lock': make_lock,
200 200 'locked_by': locked_by,
201 201 'server_url': utils2.get_server_url(environ),
202 202 'user_agent': get_user_agent(environ),
203 203 'hooks': get_enabled_hook_classes(ui_settings),
204 204 'is_shadow_repo': is_shadow_repo,
205 205 'detect_force_push': detect_force_push,
206 206 'check_branch_perms': check_branch_perms,
207 207 }
208 208 return scm_data
209 209
210 210
211 211 class BasicAuth(AuthBasicAuthenticator):
212 212
213 213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
214 214 initial_call_detection=False, acl_repo_name=None):
215 215 self.realm = realm
216 216 self.initial_call = initial_call_detection
217 217 self.authfunc = authfunc
218 218 self.registry = registry
219 219 self.acl_repo_name = acl_repo_name
220 220 self._rc_auth_http_code = auth_http_code
221 221
222 222 def _get_response_from_code(self, http_code):
223 223 try:
224 224 return get_exception(safe_int(http_code))
225 225 except Exception:
226 226 log.exception('Failed to fetch response for code %s', http_code)
227 227 return HTTPForbidden
228 228
229 229 def get_rc_realm(self):
230 230 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
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 auth_data = self.authfunc(
255 255 username, password, environ, VCS_TYPE,
256 256 registry=self.registry, acl_repo_name=self.acl_repo_name)
257 257 if auth_data:
258 258 return {'username': username, 'auth_data': auth_data}
259 259 if username and password:
260 260 # we mark that we actually executed authentication once, at
261 261 # that point we can use the alternative auth code
262 262 self.initial_call = False
263 263
264 264 return self.build_authentication()
265 265
266 266 __call__ = authenticate
267 267
268 268
269 269 def calculate_version_hash(config):
270 270 return sha1(
271 271 config.get('beaker.session.secret', '') +
272 272 rhodecode.__version__)[:8]
273 273
274 274
275 275 def get_current_lang(request):
276 276 # NOTE(marcink): remove after pyramid move
277 277 try:
278 278 return translation.get_lang()[0]
279 279 except:
280 280 pass
281 281
282 282 return getattr(request, '_LOCALE_', request.locale_name)
283 283
284 284
285 285 def attach_context_attributes(context, request, user_id=None):
286 286 """
287 287 Attach variables into template context called `c`.
288 288 """
289 289 config = request.registry.settings
290 290
291 291 rc_config = SettingsModel().get_all_settings(cache=True)
292 292
293 293 context.rhodecode_version = rhodecode.__version__
294 294 context.rhodecode_edition = config.get('rhodecode.edition')
295 295 # unique secret + version does not leak the version but keep consistency
296 296 context.rhodecode_version_hash = calculate_version_hash(config)
297 297
298 298 # Default language set for the incoming request
299 299 context.language = get_current_lang(request)
300 300
301 301 # Visual options
302 302 context.visual = AttributeDict({})
303 303
304 304 # DB stored Visual Items
305 305 context.visual.show_public_icon = str2bool(
306 306 rc_config.get('rhodecode_show_public_icon'))
307 307 context.visual.show_private_icon = str2bool(
308 308 rc_config.get('rhodecode_show_private_icon'))
309 309 context.visual.stylify_metatags = str2bool(
310 310 rc_config.get('rhodecode_stylify_metatags'))
311 311 context.visual.dashboard_items = safe_int(
312 312 rc_config.get('rhodecode_dashboard_items', 100))
313 313 context.visual.admin_grid_items = safe_int(
314 314 rc_config.get('rhodecode_admin_grid_items', 100))
315 315 context.visual.show_revision_number = str2bool(
316 316 rc_config.get('rhodecode_show_revision_number', True))
317 317 context.visual.show_sha_length = safe_int(
318 318 rc_config.get('rhodecode_show_sha_length', 100))
319 319 context.visual.repository_fields = str2bool(
320 320 rc_config.get('rhodecode_repository_fields'))
321 321 context.visual.show_version = str2bool(
322 322 rc_config.get('rhodecode_show_version'))
323 323 context.visual.use_gravatar = str2bool(
324 324 rc_config.get('rhodecode_use_gravatar'))
325 325 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
326 326 context.visual.default_renderer = rc_config.get(
327 327 'rhodecode_markup_renderer', 'rst')
328 328 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
329 329 context.visual.rhodecode_support_url = \
330 330 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
331 331
332 332 context.visual.affected_files_cut_off = 60
333 333
334 334 context.pre_code = rc_config.get('rhodecode_pre_code')
335 335 context.post_code = rc_config.get('rhodecode_post_code')
336 336 context.rhodecode_name = rc_config.get('rhodecode_title')
337 337 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
338 338 # if we have specified default_encoding in the request, it has more
339 339 # priority
340 340 if request.GET.get('default_encoding'):
341 341 context.default_encodings.insert(0, request.GET.get('default_encoding'))
342 342 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
343 343 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
344 344
345 345 # INI stored
346 346 context.labs_active = str2bool(
347 347 config.get('labs_settings_active', 'false'))
348 348 context.ssh_enabled = str2bool(
349 349 config.get('ssh.generate_authorized_keyfile', 'false'))
350 350 context.ssh_key_generator_enabled = str2bool(
351 351 config.get('ssh.enable_ui_key_generator', 'true'))
352 352
353 353 context.visual.allow_repo_location_change = str2bool(
354 354 config.get('allow_repo_location_change', True))
355 355 context.visual.allow_custom_hooks_settings = str2bool(
356 356 config.get('allow_custom_hooks_settings', True))
357 357 context.debug_style = str2bool(config.get('debug_style', False))
358 358
359 359 context.rhodecode_instanceid = config.get('instance_id')
360 360
361 361 context.visual.cut_off_limit_diff = safe_int(
362 362 config.get('cut_off_limit_diff'))
363 363 context.visual.cut_off_limit_file = safe_int(
364 364 config.get('cut_off_limit_file'))
365 365
366 366 # AppEnlight
367 367 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
368 368 context.appenlight_api_public_key = config.get(
369 369 'appenlight.api_public_key', '')
370 370 context.appenlight_server_url = config.get('appenlight.server_url', '')
371 371
372 372 diffmode = {
373 373 "unified": "unified",
374 374 "sideside": "sideside"
375 375 }.get(request.GET.get('diffmode'))
376 376
377 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
378 request.session['rc_user_session_attr.diffmode'] = diffmode
379
380 # session settings per user
377 is_api = hasattr(request, 'rpc_user')
381 378 session_attrs = {
382 379 # defaults
383 380 "clone_url_format": "http",
384 381 "diffmode": "sideside"
385 382 }
383
384 if not is_api:
385 # don't access pyramid session for API calls
386 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
387 request.session['rc_user_session_attr.diffmode'] = diffmode
388
389 # session settings per user
390
386 391 for k, v in request.session.items():
387 392 pref = 'rc_user_session_attr.'
388 393 if k and k.startswith(pref):
389 394 k = k[len(pref):]
390 395 session_attrs[k] = v
391 396
392 397 context.user_session_attrs = session_attrs
393 398
394 399 # JS template context
395 400 context.template_context = {
396 401 'repo_name': None,
397 402 'repo_type': None,
398 403 'repo_landing_commit': None,
399 404 'rhodecode_user': {
400 405 'username': None,
401 406 'email': None,
402 407 'notification_status': False
403 408 },
404 409 'session_attrs': session_attrs,
405 410 'visual': {
406 411 'default_renderer': None
407 412 },
408 413 'commit_data': {
409 414 'commit_id': None
410 415 },
411 416 'pull_request_data': {'pull_request_id': None},
412 417 'timeago': {
413 418 'refresh_time': 120 * 1000,
414 419 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
415 420 },
416 421 'pyramid_dispatch': {
417 422
418 423 },
419 424 'extra': {'plugins': {}}
420 425 }
421 426 # END CONFIG VARS
427 if is_api:
428 csrf_token = None
429 else:
430 csrf_token = auth.get_csrf_token(session=request.session)
422 431
423 context.csrf_token = auth.get_csrf_token(session=request.session)
432 context.csrf_token = csrf_token
424 433 context.backends = rhodecode.BACKENDS.keys()
425 434 context.backends.sort()
426 435 unread_count = 0
427 436 user_bookmark_list = []
428 437 if user_id:
429 438 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
430 439 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
431 440 context.unread_notifications = unread_count
432 441 context.bookmark_items = user_bookmark_list
433 442
434 443 # web case
435 444 if hasattr(request, 'user'):
436 445 context.auth_user = request.user
437 446 context.rhodecode_user = request.user
438 447
439 448 # api case
440 449 if hasattr(request, 'rpc_user'):
441 450 context.auth_user = request.rpc_user
442 451 context.rhodecode_user = request.rpc_user
443 452
444 453 # attach the whole call context to the request
445 454 request.call_context = context
446 455
447 456
448 457 def get_auth_user(request):
449 458 environ = request.environ
450 459 session = request.session
451 460
452 461 ip_addr = get_ip_addr(environ)
453 462 # make sure that we update permissions each time we call controller
454 463 _auth_token = (request.GET.get('auth_token', '') or
455 464 request.GET.get('api_key', ''))
456 465
457 466 if _auth_token:
458 467 # when using API_KEY we assume user exists, and
459 468 # doesn't need auth based on cookies.
460 469 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
461 470 authenticated = False
462 471 else:
463 472 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
464 473 try:
465 474 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
466 475 ip_addr=ip_addr)
467 476 except UserCreationError as e:
468 477 h.flash(e, 'error')
469 478 # container auth or other auth functions that create users
470 479 # on the fly can throw this exception signaling that there's
471 480 # issue with user creation, explanation should be provided
472 481 # in Exception itself. We then create a simple blank
473 482 # AuthUser
474 483 auth_user = AuthUser(ip_addr=ip_addr)
475 484
476 485 # in case someone changes a password for user it triggers session
477 486 # flush and forces a re-login
478 487 if password_changed(auth_user, session):
479 488 session.invalidate()
480 489 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
481 490 auth_user = AuthUser(ip_addr=ip_addr)
482 491
483 492 authenticated = cookie_store.get('is_authenticated')
484 493
485 494 if not auth_user.is_authenticated and auth_user.is_user_object:
486 495 # user is not authenticated and not empty
487 496 auth_user.set_authenticated(authenticated)
488 497
489 498 return auth_user
490 499
491 500
492 501 def h_filter(s):
493 502 """
494 503 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
495 504 we wrap this with additional functionality that converts None to empty
496 505 strings
497 506 """
498 507 if s is None:
499 508 return markupsafe.Markup()
500 509 return markupsafe.escape(s)
501 510
502 511
503 512 def add_events_routes(config):
504 513 """
505 514 Adds routing that can be used in events. Because some events are triggered
506 515 outside of pyramid context, we need to bootstrap request with some
507 516 routing registered
508 517 """
509 518
510 519 from rhodecode.apps._base import ADMIN_PREFIX
511 520
512 521 config.add_route(name='home', pattern='/')
513 522
514 523 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
515 524 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
516 525 config.add_route(name='repo_summary', pattern='/{repo_name}')
517 526 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
518 527 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
519 528
520 529 config.add_route(name='pullrequest_show',
521 530 pattern='/{repo_name}/pull-request/{pull_request_id}')
522 531 config.add_route(name='pull_requests_global',
523 532 pattern='/pull-request/{pull_request_id}')
524 533 config.add_route(name='repo_commit',
525 534 pattern='/{repo_name}/changeset/{commit_id}')
526 535
527 536 config.add_route(name='repo_files',
528 537 pattern='/{repo_name}/files/{commit_id}/{f_path}')
529 538
530 539
531 540 def bootstrap_config(request):
532 541 import pyramid.testing
533 542 registry = pyramid.testing.Registry('RcTestRegistry')
534 543
535 544 config = pyramid.testing.setUp(registry=registry, request=request)
536 545
537 546 # allow pyramid lookup in testing
538 547 config.include('pyramid_mako')
539 548 config.include('rhodecode.lib.rc_beaker')
540 549 config.include('rhodecode.lib.rc_cache')
541 550
542 551 add_events_routes(config)
543 552
544 553 return config
545 554
546 555
547 556 def bootstrap_request(**kwargs):
548 557 import pyramid.testing
549 558
550 559 class TestRequest(pyramid.testing.DummyRequest):
551 560 application_url = kwargs.pop('application_url', 'http://example.com')
552 561 host = kwargs.pop('host', 'example.com:80')
553 562 domain = kwargs.pop('domain', 'example.com')
554 563
555 564 def translate(self, msg):
556 565 return msg
557 566
558 567 def plularize(self, singular, plural, n):
559 568 return singular
560 569
561 570 def get_partial_renderer(self, tmpl_name):
562 571
563 572 from rhodecode.lib.partial_renderer import get_partial_renderer
564 573 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
565 574
566 575 _call_context = TemplateArgs()
567 576 _call_context.visual = TemplateArgs()
568 577 _call_context.visual.show_sha_length = 12
569 578 _call_context.visual.show_revision_number = True
570 579
571 580 @property
572 581 def call_context(self):
573 582 return self._call_context
574 583
575 584 class TestDummySession(pyramid.testing.DummySession):
576 585 def save(*arg, **kw):
577 586 pass
578 587
579 588 request = TestRequest(**kwargs)
580 589 request.session = TestDummySession()
581 590
582 591 return request
583 592
General Comments 0
You need to be logged in to leave comments. Login now