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