##// END OF EJS Templates
auth-token: expose fetched token in unified way into request attribute....
marcink -
r4002:5f150e86 default
parent child Browse files
Show More
@@ -1,592 +1,597 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 context.rc_config = rc_config
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 377 is_api = hasattr(request, 'rpc_user')
378 378 session_attrs = {
379 379 # defaults
380 380 "clone_url_format": "http",
381 381 "diffmode": "sideside"
382 382 }
383 383
384 384 if not is_api:
385 385 # don't access pyramid session for API calls
386 386 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
387 387 request.session['rc_user_session_attr.diffmode'] = diffmode
388 388
389 389 # session settings per user
390 390
391 391 for k, v in request.session.items():
392 392 pref = 'rc_user_session_attr.'
393 393 if k and k.startswith(pref):
394 394 k = k[len(pref):]
395 395 session_attrs[k] = v
396 396
397 397 context.user_session_attrs = session_attrs
398 398
399 399 # JS template context
400 400 context.template_context = {
401 401 'repo_name': None,
402 402 'repo_type': None,
403 403 'repo_landing_commit': None,
404 404 'rhodecode_user': {
405 405 'username': None,
406 406 'email': None,
407 407 'notification_status': False
408 408 },
409 409 'session_attrs': session_attrs,
410 410 'visual': {
411 411 'default_renderer': None
412 412 },
413 413 'commit_data': {
414 414 'commit_id': None
415 415 },
416 416 'pull_request_data': {'pull_request_id': None},
417 417 'timeago': {
418 418 'refresh_time': 120 * 1000,
419 419 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
420 420 },
421 421 'pyramid_dispatch': {
422 422
423 423 },
424 424 'extra': {'plugins': {}}
425 425 }
426 426 # END CONFIG VARS
427 427 if is_api:
428 428 csrf_token = None
429 429 else:
430 430 csrf_token = auth.get_csrf_token(session=request.session)
431 431
432 432 context.csrf_token = csrf_token
433 433 context.backends = rhodecode.BACKENDS.keys()
434 434 context.backends.sort()
435 435 unread_count = 0
436 436 user_bookmark_list = []
437 437 if user_id:
438 438 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
439 439 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
440 440 context.unread_notifications = unread_count
441 441 context.bookmark_items = user_bookmark_list
442 442
443 443 # web case
444 444 if hasattr(request, 'user'):
445 445 context.auth_user = request.user
446 446 context.rhodecode_user = request.user
447 447
448 448 # api case
449 449 if hasattr(request, 'rpc_user'):
450 450 context.auth_user = request.rpc_user
451 451 context.rhodecode_user = request.rpc_user
452 452
453 453 # attach the whole call context to the request
454 454 request.call_context = context
455 455
456 456
457 457 def get_auth_user(request):
458 458 environ = request.environ
459 459 session = request.session
460 460
461 461 ip_addr = get_ip_addr(environ)
462
462 463 # make sure that we update permissions each time we call controller
463 _auth_token = (request.GET.get('auth_token', '') or
464 request.GET.get('api_key', ''))
464 _auth_token = (request.GET.get('auth_token', '') or request.GET.get('api_key', ''))
465 if not _auth_token:
466 url_auth_token = request.matchdict.get('_auth_token')
467 _auth_token = url_auth_token
468 if _auth_token:
469 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
465 470
466 471 if _auth_token:
467 472 # when using API_KEY we assume user exists, and
468 473 # doesn't need auth based on cookies.
469 474 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
470 475 authenticated = False
471 476 else:
472 477 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
473 478 try:
474 479 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
475 480 ip_addr=ip_addr)
476 481 except UserCreationError as e:
477 482 h.flash(e, 'error')
478 483 # container auth or other auth functions that create users
479 484 # on the fly can throw this exception signaling that there's
480 485 # issue with user creation, explanation should be provided
481 486 # in Exception itself. We then create a simple blank
482 487 # AuthUser
483 488 auth_user = AuthUser(ip_addr=ip_addr)
484 489
485 490 # in case someone changes a password for user it triggers session
486 491 # flush and forces a re-login
487 492 if password_changed(auth_user, session):
488 493 session.invalidate()
489 494 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
490 495 auth_user = AuthUser(ip_addr=ip_addr)
491 496
492 497 authenticated = cookie_store.get('is_authenticated')
493 498
494 499 if not auth_user.is_authenticated and auth_user.is_user_object:
495 500 # user is not authenticated and not empty
496 501 auth_user.set_authenticated(authenticated)
497 502
498 return auth_user
503 return auth_user, _auth_token
499 504
500 505
501 506 def h_filter(s):
502 507 """
503 508 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
504 509 we wrap this with additional functionality that converts None to empty
505 510 strings
506 511 """
507 512 if s is None:
508 513 return markupsafe.Markup()
509 514 return markupsafe.escape(s)
510 515
511 516
512 517 def add_events_routes(config):
513 518 """
514 519 Adds routing that can be used in events. Because some events are triggered
515 520 outside of pyramid context, we need to bootstrap request with some
516 521 routing registered
517 522 """
518 523
519 524 from rhodecode.apps._base import ADMIN_PREFIX
520 525
521 526 config.add_route(name='home', pattern='/')
522 527
523 528 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
524 529 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
525 530 config.add_route(name='repo_summary', pattern='/{repo_name}')
526 531 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
527 532 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
528 533
529 534 config.add_route(name='pullrequest_show',
530 535 pattern='/{repo_name}/pull-request/{pull_request_id}')
531 536 config.add_route(name='pull_requests_global',
532 537 pattern='/pull-request/{pull_request_id}')
533 538 config.add_route(name='repo_commit',
534 539 pattern='/{repo_name}/changeset/{commit_id}')
535 540
536 541 config.add_route(name='repo_files',
537 542 pattern='/{repo_name}/files/{commit_id}/{f_path}')
538 543
539 544
540 545 def bootstrap_config(request):
541 546 import pyramid.testing
542 547 registry = pyramid.testing.Registry('RcTestRegistry')
543 548
544 549 config = pyramid.testing.setUp(registry=registry, request=request)
545 550
546 551 # allow pyramid lookup in testing
547 552 config.include('pyramid_mako')
548 553 config.include('rhodecode.lib.rc_beaker')
549 554 config.include('rhodecode.lib.rc_cache')
550 555
551 556 add_events_routes(config)
552 557
553 558 return config
554 559
555 560
556 561 def bootstrap_request(**kwargs):
557 562 import pyramid.testing
558 563
559 564 class TestRequest(pyramid.testing.DummyRequest):
560 565 application_url = kwargs.pop('application_url', 'http://example.com')
561 566 host = kwargs.pop('host', 'example.com:80')
562 567 domain = kwargs.pop('domain', 'example.com')
563 568
564 569 def translate(self, msg):
565 570 return msg
566 571
567 572 def plularize(self, singular, plural, n):
568 573 return singular
569 574
570 575 def get_partial_renderer(self, tmpl_name):
571 576
572 577 from rhodecode.lib.partial_renderer import get_partial_renderer
573 578 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
574 579
575 580 _call_context = TemplateArgs()
576 581 _call_context.visual = TemplateArgs()
577 582 _call_context.visual.show_sha_length = 12
578 583 _call_context.visual.show_revision_number = True
579 584
580 585 @property
581 586 def call_context(self):
582 587 return self._call_context
583 588
584 589 class TestDummySession(pyramid.testing.DummySession):
585 590 def save(*arg, **kw):
586 591 pass
587 592
588 593 request = TestRequest(**kwargs)
589 594 request.session = TestDummySession()
590 595
591 596 return request
592 597
@@ -1,327 +1,328 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 import io
21 21 import re
22 22 import os
23 23 import datetime
24 24 import logging
25 25 import Queue
26 26 import subprocess32
27 27
28 28
29 29 from dateutil.parser import parse
30 30 from pyramid.threadlocal import get_current_request
31 31 from pyramid.interfaces import IRoutesMapper
32 32 from pyramid.settings import asbool
33 33 from pyramid.path import AssetResolver
34 34 from threading import Thread
35 35
36 36 from rhodecode.translation import _ as tsf
37 37 from rhodecode.config.jsroutes import generate_jsroutes_content
38 38 from rhodecode.lib import auth
39 39 from rhodecode.lib.base import get_auth_user
40 40
41 41 import rhodecode
42 42
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 def add_renderer_globals(event):
48 48 from rhodecode.lib import helpers
49 49
50 50 # TODO: When executed in pyramid view context the request is not available
51 51 # in the event. Find a better solution to get the request.
52 52 request = event['request'] or get_current_request()
53 53
54 54 # Add Pyramid translation as '_' to context
55 55 event['_'] = request.translate
56 56 event['_ungettext'] = request.plularize
57 57 event['h'] = helpers
58 58
59 59
60 60 def add_localizer(event):
61 61 request = event.request
62 62 localizer = request.localizer
63 63
64 64 def auto_translate(*args, **kwargs):
65 65 return localizer.translate(tsf(*args, **kwargs))
66 66
67 67 request.translate = auto_translate
68 68 request.plularize = localizer.pluralize
69 69
70 70
71 71 def set_user_lang(event):
72 72 request = event.request
73 73 cur_user = getattr(request, 'user', None)
74 74
75 75 if cur_user:
76 76 user_lang = cur_user.get_instance().user_data.get('language')
77 77 if user_lang:
78 78 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
79 79 event.request._LOCALE_ = user_lang
80 80
81 81
82 82 def add_request_user_context(event):
83 83 """
84 84 Adds auth user into request context
85 85 """
86 86 request = event.request
87 87 # access req_id as soon as possible
88 88 req_id = request.req_id
89 89
90 90 if hasattr(request, 'vcs_call'):
91 91 # skip vcs calls
92 92 return
93 93
94 94 if hasattr(request, 'rpc_method'):
95 95 # skip api calls
96 96 return
97 97
98 auth_user = get_auth_user(request)
98 auth_user, auth_token = get_auth_user(request)
99 99 request.user = auth_user
100 request.user_auth_token = auth_token
100 101 request.environ['rc_auth_user'] = auth_user
101 102 request.environ['rc_auth_user_id'] = auth_user.user_id
102 103 request.environ['rc_req_id'] = req_id
103 104
104 105
105 106 def inject_app_settings(event):
106 107 settings = event.app.registry.settings
107 108 # inject info about available permissions
108 109 auth.set_available_permissions(settings)
109 110
110 111
111 112 def scan_repositories_if_enabled(event):
112 113 """
113 114 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
114 115 does a repository scan if enabled in the settings.
115 116 """
116 117 settings = event.app.registry.settings
117 118 vcs_server_enabled = settings['vcs.server.enable']
118 119 import_on_startup = settings['startup.import_repos']
119 120 if vcs_server_enabled and import_on_startup:
120 121 from rhodecode.model.scm import ScmModel
121 122 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
122 123 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
123 124 repo2db_mapper(repositories, remove_obsolete=False)
124 125
125 126
126 127 def write_metadata_if_needed(event):
127 128 """
128 129 Writes upgrade metadata
129 130 """
130 131 import rhodecode
131 132 from rhodecode.lib import system_info
132 133 from rhodecode.lib import ext_json
133 134
134 135 fname = '.rcmetadata.json'
135 136 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
136 137 metadata_destination = os.path.join(ini_loc, fname)
137 138
138 139 def get_update_age():
139 140 now = datetime.datetime.utcnow()
140 141
141 142 with open(metadata_destination, 'rb') as f:
142 143 data = ext_json.json.loads(f.read())
143 144 if 'created_on' in data:
144 145 update_date = parse(data['created_on'])
145 146 diff = now - update_date
146 147 return diff.total_seconds() / 60.0
147 148
148 149 return 0
149 150
150 151 def write():
151 152 configuration = system_info.SysInfo(
152 153 system_info.rhodecode_config)()['value']
153 154 license_token = configuration['config']['license_token']
154 155
155 156 setup = dict(
156 157 workers=configuration['config']['server:main'].get(
157 158 'workers', '?'),
158 159 worker_type=configuration['config']['server:main'].get(
159 160 'worker_class', 'sync'),
160 161 )
161 162 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
162 163 del dbinfo['url']
163 164
164 165 metadata = dict(
165 166 desc='upgrade metadata info',
166 167 license_token=license_token,
167 168 created_on=datetime.datetime.utcnow().isoformat(),
168 169 usage=system_info.SysInfo(system_info.usage_info)()['value'],
169 170 platform=system_info.SysInfo(system_info.platform_type)()['value'],
170 171 database=dbinfo,
171 172 cpu=system_info.SysInfo(system_info.cpu)()['value'],
172 173 memory=system_info.SysInfo(system_info.memory)()['value'],
173 174 setup=setup
174 175 )
175 176
176 177 with open(metadata_destination, 'wb') as f:
177 178 f.write(ext_json.json.dumps(metadata))
178 179
179 180 settings = event.app.registry.settings
180 181 if settings.get('metadata.skip'):
181 182 return
182 183
183 184 # only write this every 24h, workers restart caused unwanted delays
184 185 try:
185 186 age_in_min = get_update_age()
186 187 except Exception:
187 188 age_in_min = 0
188 189
189 190 if age_in_min > 60 * 60 * 24:
190 191 return
191 192
192 193 try:
193 194 write()
194 195 except Exception:
195 196 pass
196 197
197 198
198 199 def write_js_routes_if_enabled(event):
199 200 registry = event.app.registry
200 201
201 202 mapper = registry.queryUtility(IRoutesMapper)
202 203 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
203 204
204 205 def _extract_route_information(route):
205 206 """
206 207 Convert a route into tuple(name, path, args), eg:
207 208 ('show_user', '/profile/%(username)s', ['username'])
208 209 """
209 210
210 211 routepath = route.pattern
211 212 pattern = route.pattern
212 213
213 214 def replace(matchobj):
214 215 if matchobj.group(1):
215 216 return "%%(%s)s" % matchobj.group(1).split(':')[0]
216 217 else:
217 218 return "%%(%s)s" % matchobj.group(2)
218 219
219 220 routepath = _argument_prog.sub(replace, routepath)
220 221
221 222 if not routepath.startswith('/'):
222 223 routepath = '/'+routepath
223 224
224 225 return (
225 226 route.name,
226 227 routepath,
227 228 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
228 229 for arg in _argument_prog.findall(pattern)]
229 230 )
230 231
231 232 def get_routes():
232 233 # pyramid routes
233 234 for route in mapper.get_routes():
234 235 if not route.name.startswith('__'):
235 236 yield _extract_route_information(route)
236 237
237 238 if asbool(registry.settings.get('generate_js_files', 'false')):
238 239 static_path = AssetResolver().resolve('rhodecode:public').abspath()
239 240 jsroutes = get_routes()
240 241 jsroutes_file_content = generate_jsroutes_content(jsroutes)
241 242 jsroutes_file_path = os.path.join(
242 243 static_path, 'js', 'rhodecode', 'routes.js')
243 244
244 245 try:
245 246 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
246 247 f.write(jsroutes_file_content)
247 248 except Exception:
248 249 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
249 250
250 251
251 252 class Subscriber(object):
252 253 """
253 254 Base class for subscribers to the pyramid event system.
254 255 """
255 256 def __call__(self, event):
256 257 self.run(event)
257 258
258 259 def run(self, event):
259 260 raise NotImplementedError('Subclass has to implement this.')
260 261
261 262
262 263 class AsyncSubscriber(Subscriber):
263 264 """
264 265 Subscriber that handles the execution of events in a separate task to not
265 266 block the execution of the code which triggers the event. It puts the
266 267 received events into a queue from which the worker process takes them in
267 268 order.
268 269 """
269 270 def __init__(self):
270 271 self._stop = False
271 272 self._eventq = Queue.Queue()
272 273 self._worker = self.create_worker()
273 274 self._worker.start()
274 275
275 276 def __call__(self, event):
276 277 self._eventq.put(event)
277 278
278 279 def create_worker(self):
279 280 worker = Thread(target=self.do_work)
280 281 worker.daemon = True
281 282 return worker
282 283
283 284 def stop_worker(self):
284 285 self._stop = False
285 286 self._eventq.put(None)
286 287 self._worker.join()
287 288
288 289 def do_work(self):
289 290 while not self._stop:
290 291 event = self._eventq.get()
291 292 if event is not None:
292 293 self.run(event)
293 294
294 295
295 296 class AsyncSubprocessSubscriber(AsyncSubscriber):
296 297 """
297 298 Subscriber that uses the subprocess32 module to execute a command if an
298 299 event is received. Events are handled asynchronously.
299 300 """
300 301
301 302 def __init__(self, cmd, timeout=None):
302 303 super(AsyncSubprocessSubscriber, self).__init__()
303 304 self._cmd = cmd
304 305 self._timeout = timeout
305 306
306 307 def run(self, event):
307 308 cmd = self._cmd
308 309 timeout = self._timeout
309 310 log.debug('Executing command %s.', cmd)
310 311
311 312 try:
312 313 output = subprocess32.check_output(
313 314 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
314 315 log.debug('Command finished %s', cmd)
315 316 if output:
316 317 log.debug('Command output: %s', output)
317 318 except subprocess32.TimeoutExpired as e:
318 319 log.exception('Timeout while executing command.')
319 320 if e.output:
320 321 log.error('Command output: %s', e.output)
321 322 except subprocess32.CalledProcessError as e:
322 323 log.exception('Error while executing command.')
323 324 if e.output:
324 325 log.error('Command output: %s', e.output)
325 326 except:
326 327 log.exception(
327 328 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now