##// END OF EJS Templates
simplevcs: allow passing config into repo detection logic....
marcink -
r2519:c5a11bd9 stable
parent child Browse files
Show More
@@ -1,641 +1,644 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31
32 32 import time
33 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 34 # TODO(marcink): check if we should use webob.exc here ?
35 35 from pyramid.httpexceptions import (
36 36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 37 from zope.cachedescriptors.property import Lazy as LazyProperty
38 38
39 39 import rhodecode
40 40 from rhodecode.authentication.base import (
41 41 authenticate, get_perms_cache_manager, VCS_TYPE, loadplugin)
42 42 from rhodecode.lib import caches
43 43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 44 from rhodecode.lib.base import (
45 45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 46 from rhodecode.lib.exceptions import (
47 47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
48 48 NotAllowedToCreateUserError)
49 49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
50 50 from rhodecode.lib.middleware import appenlight
51 51 from rhodecode.lib.middleware.utils import scm_app_http
52 52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
53 53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
54 54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 55 from rhodecode.lib.vcs.backends import base
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def initialize_generator(factory):
66 66 """
67 67 Initializes the returned generator by draining its first element.
68 68
69 69 This can be used to give a generator an initializer, which is the code
70 70 up to the first yield statement. This decorator enforces that the first
71 71 produced element has the value ``"__init__"`` to make its special
72 72 purpose very explicit in the using code.
73 73 """
74 74
75 75 @wraps(factory)
76 76 def wrapper(*args, **kwargs):
77 77 gen = factory(*args, **kwargs)
78 78 try:
79 79 init = gen.next()
80 80 except StopIteration:
81 81 raise ValueError('Generator must yield at least one element.')
82 82 if init != "__init__":
83 83 raise ValueError('First yielded element must be "__init__".')
84 84 return gen
85 85 return wrapper
86 86
87 87
88 88 class SimpleVCS(object):
89 89 """Common functionality for SCM HTTP handlers."""
90 90
91 91 SCM = 'unknown'
92 92
93 93 acl_repo_name = None
94 94 url_repo_name = None
95 95 vcs_repo_name = None
96 96 rc_extras = {}
97 97
98 98 # We have to handle requests to shadow repositories different than requests
99 99 # to normal repositories. Therefore we have to distinguish them. To do this
100 100 # we use this regex which will match only on URLs pointing to shadow
101 101 # repositories.
102 102 shadow_repo_re = re.compile(
103 103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
104 104 '(?P<target>{slug_pat})/' # target repo
105 105 'pull-request/(?P<pr_id>\d+)/' # pull request
106 106 'repository$' # shadow repo
107 107 .format(slug_pat=SLUG_RE.pattern))
108 108
109 109 def __init__(self, config, registry):
110 110 self.registry = registry
111 111 self.config = config
112 112 # re-populated by specialized middleware
113 113 self.repo_vcs_config = base.Config()
114 114 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
115 115
116 116 registry.rhodecode_settings = self.rhodecode_settings
117 117 # authenticate this VCS request using authfunc
118 118 auth_ret_code_detection = \
119 119 str2bool(self.config.get('auth_ret_code_detection', False))
120 120 self.authenticate = BasicAuth(
121 121 '', authenticate, registry, config.get('auth_ret_code'),
122 122 auth_ret_code_detection)
123 123 self.ip_addr = '0.0.0.0'
124 124
125 125 @LazyProperty
126 126 def global_vcs_config(self):
127 127 try:
128 128 return VcsSettingsModel().get_ui_settings_as_config_obj()
129 129 except Exception:
130 130 return base.Config()
131 131
132 132 @property
133 133 def base_path(self):
134 134 settings_path = self.repo_vcs_config.get(
135 135 *VcsSettingsModel.PATH_SETTING)
136 136
137 137 if not settings_path:
138 138 settings_path = self.global_vcs_config.get(
139 139 *VcsSettingsModel.PATH_SETTING)
140 140
141 141 if not settings_path:
142 142 # try, maybe we passed in explicitly as config option
143 143 settings_path = self.config.get('base_path')
144 144
145 145 if not settings_path:
146 146 raise ValueError('FATAL: base_path is empty')
147 147 return settings_path
148 148
149 149 def set_repo_names(self, environ):
150 150 """
151 151 This will populate the attributes acl_repo_name, url_repo_name,
152 152 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
153 153 shadow) repositories all names are equal. In case of requests to a
154 154 shadow repository the acl-name points to the target repo of the pull
155 155 request and the vcs-name points to the shadow repo file system path.
156 156 The url-name is always the URL used by the vcs client program.
157 157
158 158 Example in case of a shadow repo:
159 159 acl_repo_name = RepoGroup/MyRepo
160 160 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
161 161 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
162 162 """
163 163 # First we set the repo name from URL for all attributes. This is the
164 164 # default if handling normal (non shadow) repo requests.
165 165 self.url_repo_name = self._get_repository_name(environ)
166 166 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
167 167 self.is_shadow_repo = False
168 168
169 169 # Check if this is a request to a shadow repository.
170 170 match = self.shadow_repo_re.match(self.url_repo_name)
171 171 if match:
172 172 match_dict = match.groupdict()
173 173
174 174 # Build acl repo name from regex match.
175 175 acl_repo_name = safe_unicode('{groups}{target}'.format(
176 176 groups=match_dict['groups'] or '',
177 177 target=match_dict['target']))
178 178
179 179 # Retrieve pull request instance by ID from regex match.
180 180 pull_request = PullRequest.get(match_dict['pr_id'])
181 181
182 182 # Only proceed if we got a pull request and if acl repo name from
183 183 # URL equals the target repo name of the pull request.
184 184 if pull_request and (acl_repo_name ==
185 185 pull_request.target_repo.repo_name):
186 186 # Get file system path to shadow repository.
187 187 workspace_id = PullRequestModel()._workspace_id(pull_request)
188 188 target_vcs = pull_request.target_repo.scm_instance()
189 189 vcs_repo_name = target_vcs._get_shadow_repository_path(
190 190 workspace_id)
191 191
192 192 # Store names for later usage.
193 193 self.vcs_repo_name = vcs_repo_name
194 194 self.acl_repo_name = acl_repo_name
195 195 self.is_shadow_repo = True
196 196
197 197 log.debug('Setting all VCS repository names: %s', {
198 198 'acl_repo_name': self.acl_repo_name,
199 199 'url_repo_name': self.url_repo_name,
200 200 'vcs_repo_name': self.vcs_repo_name,
201 201 })
202 202
203 203 @property
204 204 def scm_app(self):
205 205 custom_implementation = self.config['vcs.scm_app_implementation']
206 206 if custom_implementation == 'http':
207 207 log.info('Using HTTP implementation of scm app.')
208 208 scm_app_impl = scm_app_http
209 209 else:
210 210 log.info('Using custom implementation of scm_app: "{}"'.format(
211 211 custom_implementation))
212 212 scm_app_impl = importlib.import_module(custom_implementation)
213 213 return scm_app_impl
214 214
215 215 def _get_by_id(self, repo_name):
216 216 """
217 217 Gets a special pattern _<ID> from clone url and tries to replace it
218 218 with a repository_name for support of _<ID> non changeable urls
219 219 """
220 220
221 221 data = repo_name.split('/')
222 222 if len(data) >= 2:
223 223 from rhodecode.model.repo import RepoModel
224 224 by_id_match = RepoModel().get_repo_by_id(repo_name)
225 225 if by_id_match:
226 226 data[1] = by_id_match.repo_name
227 227
228 228 return safe_str('/'.join(data))
229 229
230 230 def _invalidate_cache(self, repo_name):
231 231 """
232 232 Set's cache for this repository for invalidation on next access
233 233
234 234 :param repo_name: full repo name, also a cache key
235 235 """
236 236 ScmModel().mark_for_invalidation(repo_name)
237 237
238 238 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
239 239 db_repo = Repository.get_by_repo_name(repo_name)
240 240 if not db_repo:
241 241 log.debug('Repository `%s` not found inside the database.',
242 242 repo_name)
243 243 return False
244 244
245 245 if db_repo.repo_type != scm_type:
246 246 log.warning(
247 247 'Repository `%s` have incorrect scm_type, expected %s got %s',
248 248 repo_name, db_repo.repo_type, scm_type)
249 249 return False
250 250
251 return is_valid_repo(repo_name, base_path,
252 explicit_scm=scm_type, expect_scm=scm_type)
251 config = db_repo._config
252 config.set('extensions', 'largefiles', '')
253 return is_valid_repo(
254 repo_name, base_path,
255 explicit_scm=scm_type, expect_scm=scm_type, config=config)
253 256
254 257 def valid_and_active_user(self, user):
255 258 """
256 259 Checks if that user is not empty, and if it's actually object it checks
257 260 if he's active.
258 261
259 262 :param user: user object or None
260 263 :return: boolean
261 264 """
262 265 if user is None:
263 266 return False
264 267
265 268 elif user.active:
266 269 return True
267 270
268 271 return False
269 272
270 273 @property
271 274 def is_shadow_repo_dir(self):
272 275 return os.path.isdir(self.vcs_repo_name)
273 276
274 277 def _check_permission(self, action, user, repo_name, ip_addr=None,
275 278 plugin_id='', plugin_cache_active=False, cache_ttl=0):
276 279 """
277 280 Checks permissions using action (push/pull) user and repository
278 281 name. If plugin_cache and ttl is set it will use the plugin which
279 282 authenticated the user to store the cached permissions result for N
280 283 amount of seconds as in cache_ttl
281 284
282 285 :param action: push or pull action
283 286 :param user: user instance
284 287 :param repo_name: repository name
285 288 """
286 289
287 290 # get instance of cache manager configured for a namespace
288 291 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
289 292 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
290 293 plugin_id, plugin_cache_active, cache_ttl)
291 294
292 295 # for environ based password can be empty, but then the validation is
293 296 # on the server that fills in the env data needed for authentication
294 297 _perm_calc_hash = caches.compute_key_from_params(
295 298 plugin_id, action, user.user_id, repo_name, ip_addr)
296 299
297 300 # _authenticate is a wrapper for .auth() method of plugin.
298 301 # it checks if .auth() sends proper data.
299 302 # For RhodeCodeExternalAuthPlugin it also maps users to
300 303 # Database and maps the attributes returned from .auth()
301 304 # to RhodeCode database. If this function returns data
302 305 # then auth is correct.
303 306 start = time.time()
304 307 log.debug('Running plugin `%s` permissions check', plugin_id)
305 308
306 309 def perm_func():
307 310 """
308 311 This function is used internally in Cache of Beaker to calculate
309 312 Results
310 313 """
311 314 log.debug('auth: calculating permission access now...')
312 315 # check IP
313 316 inherit = user.inherit_default_permissions
314 317 ip_allowed = AuthUser.check_ip_allowed(
315 318 user.user_id, ip_addr, inherit_from_default=inherit)
316 319 if ip_allowed:
317 320 log.info('Access for IP:%s allowed', ip_addr)
318 321 else:
319 322 return False
320 323
321 324 if action == 'push':
322 325 perms = ('repository.write', 'repository.admin')
323 326 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
324 327 return False
325 328
326 329 else:
327 330 # any other action need at least read permission
328 331 perms = (
329 332 'repository.read', 'repository.write', 'repository.admin')
330 333 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
331 334 return False
332 335
333 336 return True
334 337
335 338 if plugin_cache_active:
336 339 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
337 340 perm_result = cache_manager.get(
338 341 _perm_calc_hash, createfunc=perm_func)
339 342 else:
340 343 perm_result = perm_func()
341 344
342 345 auth_time = time.time() - start
343 346 log.debug('Permissions for plugin `%s` completed in %.3fs, '
344 347 'expiration time of fetched cache %.1fs.',
345 348 plugin_id, auth_time, cache_ttl)
346 349
347 350 return perm_result
348 351
349 352 def _check_ssl(self, environ, start_response):
350 353 """
351 354 Checks the SSL check flag and returns False if SSL is not present
352 355 and required True otherwise
353 356 """
354 357 org_proto = environ['wsgi._org_proto']
355 358 # check if we have SSL required ! if not it's a bad request !
356 359 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
357 360 if require_ssl and org_proto == 'http':
358 361 log.debug('proto is %s and SSL is required BAD REQUEST !',
359 362 org_proto)
360 363 return False
361 364 return True
362 365
363 366 def _get_default_cache_ttl(self):
364 367 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
365 368 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
366 369 plugin_settings = plugin.get_settings()
367 370 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
368 371 plugin_settings) or (False, 0)
369 372 return plugin_cache_active, cache_ttl
370 373
371 374 def __call__(self, environ, start_response):
372 375 try:
373 376 return self._handle_request(environ, start_response)
374 377 except Exception:
375 378 log.exception("Exception while handling request")
376 379 appenlight.track_exception(environ)
377 380 return HTTPInternalServerError()(environ, start_response)
378 381 finally:
379 382 meta.Session.remove()
380 383
381 384 def _handle_request(self, environ, start_response):
382 385
383 386 if not self._check_ssl(environ, start_response):
384 387 reason = ('SSL required, while RhodeCode was unable '
385 388 'to detect this as SSL request')
386 389 log.debug('User not allowed to proceed, %s', reason)
387 390 return HTTPNotAcceptable(reason)(environ, start_response)
388 391
389 392 if not self.url_repo_name:
390 393 log.warning('Repository name is empty: %s', self.url_repo_name)
391 394 # failed to get repo name, we fail now
392 395 return HTTPNotFound()(environ, start_response)
393 396 log.debug('Extracted repo name is %s', self.url_repo_name)
394 397
395 398 ip_addr = get_ip_addr(environ)
396 399 user_agent = get_user_agent(environ)
397 400 username = None
398 401
399 402 # skip passing error to error controller
400 403 environ['pylons.status_code_redirect'] = True
401 404
402 405 # ======================================================================
403 406 # GET ACTION PULL or PUSH
404 407 # ======================================================================
405 408 action = self._get_action(environ)
406 409
407 410 # ======================================================================
408 411 # Check if this is a request to a shadow repository of a pull request.
409 412 # In this case only pull action is allowed.
410 413 # ======================================================================
411 414 if self.is_shadow_repo and action != 'pull':
412 415 reason = 'Only pull action is allowed for shadow repositories.'
413 416 log.debug('User not allowed to proceed, %s', reason)
414 417 return HTTPNotAcceptable(reason)(environ, start_response)
415 418
416 419 # Check if the shadow repo actually exists, in case someone refers
417 420 # to it, and it has been deleted because of successful merge.
418 421 if self.is_shadow_repo and not self.is_shadow_repo_dir:
419 422 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
420 423 self.is_shadow_repo_dir)
421 424 return HTTPNotFound()(environ, start_response)
422 425
423 426 # ======================================================================
424 427 # CHECK ANONYMOUS PERMISSION
425 428 # ======================================================================
426 429 if action in ['pull', 'push']:
427 430 anonymous_user = User.get_default_user()
428 431 username = anonymous_user.username
429 432 if anonymous_user.active:
430 433 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
431 434 # ONLY check permissions if the user is activated
432 435 anonymous_perm = self._check_permission(
433 436 action, anonymous_user, self.acl_repo_name, ip_addr,
434 437 plugin_id='anonymous_access',
435 438 plugin_cache_active=plugin_cache_active, cache_ttl=cache_ttl,
436 439 )
437 440 else:
438 441 anonymous_perm = False
439 442
440 443 if not anonymous_user.active or not anonymous_perm:
441 444 if not anonymous_user.active:
442 445 log.debug('Anonymous access is disabled, running '
443 446 'authentication')
444 447
445 448 if not anonymous_perm:
446 449 log.debug('Not enough credentials to access this '
447 450 'repository as anonymous user')
448 451
449 452 username = None
450 453 # ==============================================================
451 454 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
452 455 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
453 456 # ==============================================================
454 457
455 458 # try to auth based on environ, container auth methods
456 459 log.debug('Running PRE-AUTH for container based authentication')
457 460 pre_auth = authenticate(
458 461 '', '', environ, VCS_TYPE, registry=self.registry,
459 462 acl_repo_name=self.acl_repo_name)
460 463 if pre_auth and pre_auth.get('username'):
461 464 username = pre_auth['username']
462 465 log.debug('PRE-AUTH got %s as username', username)
463 466 if pre_auth:
464 467 log.debug('PRE-AUTH successful from %s',
465 468 pre_auth.get('auth_data', {}).get('_plugin'))
466 469
467 470 # If not authenticated by the container, running basic auth
468 471 # before inject the calling repo_name for special scope checks
469 472 self.authenticate.acl_repo_name = self.acl_repo_name
470 473
471 474 plugin_cache_active, cache_ttl = False, 0
472 475 plugin = None
473 476 if not username:
474 477 self.authenticate.realm = self.authenticate.get_rc_realm()
475 478
476 479 try:
477 480 auth_result = self.authenticate(environ)
478 481 except (UserCreationError, NotAllowedToCreateUserError) as e:
479 482 log.error(e)
480 483 reason = safe_str(e)
481 484 return HTTPNotAcceptable(reason)(environ, start_response)
482 485
483 486 if isinstance(auth_result, dict):
484 487 AUTH_TYPE.update(environ, 'basic')
485 488 REMOTE_USER.update(environ, auth_result['username'])
486 489 username = auth_result['username']
487 490 plugin = auth_result.get('auth_data', {}).get('_plugin')
488 491 log.info(
489 492 'MAIN-AUTH successful for user `%s` from %s plugin',
490 493 username, plugin)
491 494
492 495 plugin_cache_active, cache_ttl = auth_result.get(
493 496 'auth_data', {}).get('_ttl_cache') or (False, 0)
494 497 else:
495 498 return auth_result.wsgi_application(
496 499 environ, start_response)
497 500
498 501
499 502 # ==============================================================
500 503 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
501 504 # ==============================================================
502 505 user = User.get_by_username(username)
503 506 if not self.valid_and_active_user(user):
504 507 return HTTPForbidden()(environ, start_response)
505 508 username = user.username
506 509 user.update_lastactivity()
507 510 meta.Session().commit()
508 511
509 512 # check user attributes for password change flag
510 513 user_obj = user
511 514 if user_obj and user_obj.username != User.DEFAULT_USER and \
512 515 user_obj.user_data.get('force_password_change'):
513 516 reason = 'password change required'
514 517 log.debug('User not allowed to authenticate, %s', reason)
515 518 return HTTPNotAcceptable(reason)(environ, start_response)
516 519
517 520 # check permissions for this repository
518 521 perm = self._check_permission(
519 522 action, user, self.acl_repo_name, ip_addr,
520 523 plugin, plugin_cache_active, cache_ttl)
521 524 if not perm:
522 525 return HTTPForbidden()(environ, start_response)
523 526
524 527 # extras are injected into UI object and later available
525 528 # in hooks executed by RhodeCode
526 529 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
527 530 extras = vcs_operation_context(
528 531 environ, repo_name=self.acl_repo_name, username=username,
529 532 action=action, scm=self.SCM, check_locking=check_locking,
530 533 is_shadow_repo=self.is_shadow_repo
531 534 )
532 535
533 536 # ======================================================================
534 537 # REQUEST HANDLING
535 538 # ======================================================================
536 539 repo_path = os.path.join(
537 540 safe_str(self.base_path), safe_str(self.vcs_repo_name))
538 541 log.debug('Repository path is %s', repo_path)
539 542
540 543 fix_PATH()
541 544
542 545 log.info(
543 546 '%s action on %s repo "%s" by "%s" from %s %s',
544 547 action, self.SCM, safe_str(self.url_repo_name),
545 548 safe_str(username), ip_addr, user_agent)
546 549
547 550 return self._generate_vcs_response(
548 551 environ, start_response, repo_path, extras, action)
549 552
550 553 @initialize_generator
551 554 def _generate_vcs_response(
552 555 self, environ, start_response, repo_path, extras, action):
553 556 """
554 557 Returns a generator for the response content.
555 558
556 559 This method is implemented as a generator, so that it can trigger
557 560 the cache validation after all content sent back to the client. It
558 561 also handles the locking exceptions which will be triggered when
559 562 the first chunk is produced by the underlying WSGI application.
560 563 """
561 564 callback_daemon, extras = self._prepare_callback_daemon(extras)
562 565 config = self._create_config(extras, self.acl_repo_name)
563 566 log.debug('HOOKS extras is %s', extras)
564 567 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
565 568 app.rc_extras = extras
566 569
567 570 try:
568 571 with callback_daemon:
569 572 try:
570 573 response = app(environ, start_response)
571 574 finally:
572 575 # This statement works together with the decorator
573 576 # "initialize_generator" above. The decorator ensures that
574 577 # we hit the first yield statement before the generator is
575 578 # returned back to the WSGI server. This is needed to
576 579 # ensure that the call to "app" above triggers the
577 580 # needed callback to "start_response" before the
578 581 # generator is actually used.
579 582 yield "__init__"
580 583
581 584 for chunk in response:
582 585 yield chunk
583 586 except Exception as exc:
584 587 # TODO: martinb: Exceptions are only raised in case of the Pyro4
585 588 # backend. Refactor this except block after dropping Pyro4 support.
586 589 # TODO: johbo: Improve "translating" back the exception.
587 590 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
588 591 exc = HTTPLockedRC(*exc.args)
589 592 _code = rhodecode.CONFIG.get('lock_ret_code')
590 593 log.debug('Repository LOCKED ret code %s!', (_code,))
591 594 elif getattr(exc, '_vcs_kind', None) == 'requirement':
592 595 log.debug(
593 596 'Repository requires features unknown to this Mercurial')
594 597 exc = HTTPRequirementError(*exc.args)
595 598 else:
596 599 raise
597 600
598 601 for chunk in exc(environ, start_response):
599 602 yield chunk
600 603 finally:
601 604 # invalidate cache on push
602 605 try:
603 606 if action == 'push':
604 607 self._invalidate_cache(self.url_repo_name)
605 608 finally:
606 609 meta.Session.remove()
607 610
608 611 def _get_repository_name(self, environ):
609 612 """Get repository name out of the environmnent
610 613
611 614 :param environ: WSGI environment
612 615 """
613 616 raise NotImplementedError()
614 617
615 618 def _get_action(self, environ):
616 619 """Map request commands into a pull or push command.
617 620
618 621 :param environ: WSGI environment
619 622 """
620 623 raise NotImplementedError()
621 624
622 625 def _create_wsgi_app(self, repo_path, repo_name, config):
623 626 """Return the WSGI app that will finally handle the request."""
624 627 raise NotImplementedError()
625 628
626 629 def _create_config(self, extras, repo_name):
627 630 """Create a safe config representation."""
628 631 raise NotImplementedError()
629 632
630 633 def _prepare_callback_daemon(self, extras):
631 634 return prepare_callback_daemon(
632 635 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
633 636 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
634 637
635 638
636 639 def _should_check_locking(query_string):
637 640 # this is kind of hacky, but due to how mercurial handles client-server
638 641 # server see all operation on commit; bookmarks, phases and
639 642 # obsolescence marker in different transaction, we don't want to check
640 643 # locking on those
641 644 return query_string not in ['cmd=listkeys']
@@ -1,773 +1,775 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 Utilities library for RhodeCode
23 23 """
24 24
25 25 import datetime
26 26 import decorator
27 27 import json
28 28 import logging
29 29 import os
30 30 import re
31 31 import shutil
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35 import warnings
36 36 import hashlib
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from webhelpers.text import collapse, remove_formatting, strip_tags
42 42 from mako import exceptions
43 43 from pyramid.threadlocal import get_current_registry
44 44 from pyramid.request import Request
45 45
46 46 from rhodecode.lib.fakemod import create_module
47 47 from rhodecode.lib.vcs.backends.base import Config
48 48 from rhodecode.lib.vcs.exceptions import VCSError
49 49 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
50 50 from rhodecode.lib.utils2 import (
51 51 safe_str, safe_unicode, get_current_rhodecode_user, md5)
52 52 from rhodecode.model import meta
53 53 from rhodecode.model.db import (
54 54 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 55 from rhodecode.model.meta import Session
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
61 61
62 62 # String which contains characters that are not allowed in slug names for
63 63 # repositories or repository groups. It is properly escaped to use it in
64 64 # regular expressions.
65 65 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
66 66
67 67 # Regex that matches forbidden characters in repo/group slugs.
68 68 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
69 69
70 70 # Regex that matches allowed characters in repo/group slugs.
71 71 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
72 72
73 73 # Regex that matches whole repo/group slugs.
74 74 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
75 75
76 76 _license_cache = None
77 77
78 78
79 79 def repo_name_slug(value):
80 80 """
81 81 Return slug of name of repository
82 82 This function is called on each creation/modification
83 83 of repository to prevent bad names in repo
84 84 """
85 85 replacement_char = '-'
86 86
87 87 slug = remove_formatting(value)
88 88 slug = SLUG_BAD_CHAR_RE.sub('', slug)
89 89 slug = re.sub('[\s]+', '-', slug)
90 90 slug = collapse(slug, replacement_char)
91 91 return slug
92 92
93 93
94 94 #==============================================================================
95 95 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
96 96 #==============================================================================
97 97 def get_repo_slug(request):
98 98 _repo = ''
99 99
100 100 if hasattr(request, 'db_repo'):
101 101 # if our requests has set db reference use it for name, this
102 102 # translates the example.com/_<id> into proper repo names
103 103 _repo = request.db_repo.repo_name
104 104 elif getattr(request, 'matchdict', None):
105 105 # pyramid
106 106 _repo = request.matchdict.get('repo_name')
107 107
108 108 if _repo:
109 109 _repo = _repo.rstrip('/')
110 110 return _repo
111 111
112 112
113 113 def get_repo_group_slug(request):
114 114 _group = ''
115 115 if hasattr(request, 'db_repo_group'):
116 116 # if our requests has set db reference use it for name, this
117 117 # translates the example.com/_<id> into proper repo group names
118 118 _group = request.db_repo_group.group_name
119 119 elif getattr(request, 'matchdict', None):
120 120 # pyramid
121 121 _group = request.matchdict.get('repo_group_name')
122 122
123 123
124 124 if _group:
125 125 _group = _group.rstrip('/')
126 126 return _group
127 127
128 128
129 129 def get_user_group_slug(request):
130 130 _user_group = ''
131 131
132 132 if hasattr(request, 'db_user_group'):
133 133 _user_group = request.db_user_group.users_group_name
134 134 elif getattr(request, 'matchdict', None):
135 135 # pyramid
136 136 _user_group = request.matchdict.get('user_group_id')
137 137
138 138 try:
139 139 _user_group = UserGroup.get(_user_group)
140 140 if _user_group:
141 141 _user_group = _user_group.users_group_name
142 142 except Exception:
143 143 log.exception('Failed to get user group by id')
144 144 # catch all failures here
145 145 return None
146 146
147 147 return _user_group
148 148
149 149
150 150 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
151 151 """
152 152 Scans given path for repos and return (name,(type,path)) tuple
153 153
154 154 :param path: path to scan for repositories
155 155 :param recursive: recursive search and return names with subdirs in front
156 156 """
157 157
158 158 # remove ending slash for better results
159 159 path = path.rstrip(os.sep)
160 160 log.debug('now scanning in %s location recursive:%s...', path, recursive)
161 161
162 162 def _get_repos(p):
163 163 dirpaths = _get_dirpaths(p)
164 164 if not _is_dir_writable(p):
165 165 log.warning('repo path without write access: %s', p)
166 166
167 167 for dirpath in dirpaths:
168 168 if os.path.isfile(os.path.join(p, dirpath)):
169 169 continue
170 170 cur_path = os.path.join(p, dirpath)
171 171
172 172 # skip removed repos
173 173 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
174 174 continue
175 175
176 176 #skip .<somethin> dirs
177 177 if dirpath.startswith('.'):
178 178 continue
179 179
180 180 try:
181 181 scm_info = get_scm(cur_path)
182 182 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
183 183 except VCSError:
184 184 if not recursive:
185 185 continue
186 186 #check if this dir containts other repos for recursive scan
187 187 rec_path = os.path.join(p, dirpath)
188 188 if os.path.isdir(rec_path):
189 189 for inner_scm in _get_repos(rec_path):
190 190 yield inner_scm
191 191
192 192 return _get_repos(path)
193 193
194 194
195 195 def _get_dirpaths(p):
196 196 try:
197 197 # OS-independable way of checking if we have at least read-only
198 198 # access or not.
199 199 dirpaths = os.listdir(p)
200 200 except OSError:
201 201 log.warning('ignoring repo path without read access: %s', p)
202 202 return []
203 203
204 204 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
205 205 # decode paths and suddenly returns unicode objects itself. The items it
206 206 # cannot decode are returned as strings and cause issues.
207 207 #
208 208 # Those paths are ignored here until a solid solution for path handling has
209 209 # been built.
210 210 expected_type = type(p)
211 211
212 212 def _has_correct_type(item):
213 213 if type(item) is not expected_type:
214 214 log.error(
215 215 u"Ignoring path %s since it cannot be decoded into unicode.",
216 216 # Using "repr" to make sure that we see the byte value in case
217 217 # of support.
218 218 repr(item))
219 219 return False
220 220 return True
221 221
222 222 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
223 223
224 224 return dirpaths
225 225
226 226
227 227 def _is_dir_writable(path):
228 228 """
229 229 Probe if `path` is writable.
230 230
231 231 Due to trouble on Cygwin / Windows, this is actually probing if it is
232 232 possible to create a file inside of `path`, stat does not produce reliable
233 233 results in this case.
234 234 """
235 235 try:
236 236 with tempfile.TemporaryFile(dir=path):
237 237 pass
238 238 except OSError:
239 239 return False
240 240 return True
241 241
242 242
243 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
243 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
244 244 """
245 245 Returns True if given path is a valid repository False otherwise.
246 246 If expect_scm param is given also, compare if given scm is the same
247 247 as expected from scm parameter. If explicit_scm is given don't try to
248 248 detect the scm, just use the given one to check if repo is valid
249 249
250 250 :param repo_name:
251 251 :param base_path:
252 252 :param expect_scm:
253 253 :param explicit_scm:
254 :param config:
254 255
255 256 :return True: if given path is a valid repository
256 257 """
257 258 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
258 259 log.debug('Checking if `%s` is a valid path for repository. '
259 260 'Explicit type: %s', repo_name, explicit_scm)
260 261
261 262 try:
262 263 if explicit_scm:
263 detected_scms = [get_scm_backend(explicit_scm)(full_path).alias]
264 detected_scms = [get_scm_backend(explicit_scm)(
265 full_path, config=config).alias]
264 266 else:
265 267 detected_scms = get_scm(full_path)
266 268
267 269 if expect_scm:
268 270 return detected_scms[0] == expect_scm
269 271 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
270 272 return True
271 273 except VCSError:
272 274 log.debug('path: %s is not a valid repo !', full_path)
273 275 return False
274 276
275 277
276 278 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
277 279 """
278 280 Returns True if given path is a repository group, False otherwise
279 281
280 282 :param repo_name:
281 283 :param base_path:
282 284 """
283 285 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
284 286 log.debug('Checking if `%s` is a valid path for repository group',
285 287 repo_group_name)
286 288
287 289 # check if it's not a repo
288 290 if is_valid_repo(repo_group_name, base_path):
289 291 log.debug('Repo called %s exist, it is not a valid '
290 292 'repo group' % repo_group_name)
291 293 return False
292 294
293 295 try:
294 296 # we need to check bare git repos at higher level
295 297 # since we might match branches/hooks/info/objects or possible
296 298 # other things inside bare git repo
297 299 scm_ = get_scm(os.path.dirname(full_path))
298 300 log.debug('path: %s is a vcs object:%s, not valid '
299 301 'repo group' % (full_path, scm_))
300 302 return False
301 303 except VCSError:
302 304 pass
303 305
304 306 # check if it's a valid path
305 307 if skip_path_check or os.path.isdir(full_path):
306 308 log.debug('path: %s is a valid repo group !', full_path)
307 309 return True
308 310
309 311 log.debug('path: %s is not a valid repo group !', full_path)
310 312 return False
311 313
312 314
313 315 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
314 316 while True:
315 317 ok = raw_input(prompt)
316 318 if ok.lower() in ('y', 'ye', 'yes'):
317 319 return True
318 320 if ok.lower() in ('n', 'no', 'nop', 'nope'):
319 321 return False
320 322 retries = retries - 1
321 323 if retries < 0:
322 324 raise IOError
323 325 print(complaint)
324 326
325 327 # propagated from mercurial documentation
326 328 ui_sections = [
327 329 'alias', 'auth',
328 330 'decode/encode', 'defaults',
329 331 'diff', 'email',
330 332 'extensions', 'format',
331 333 'merge-patterns', 'merge-tools',
332 334 'hooks', 'http_proxy',
333 335 'smtp', 'patch',
334 336 'paths', 'profiling',
335 337 'server', 'trusted',
336 338 'ui', 'web', ]
337 339
338 340
339 341 def config_data_from_db(clear_session=True, repo=None):
340 342 """
341 343 Read the configuration data from the database and return configuration
342 344 tuples.
343 345 """
344 346 from rhodecode.model.settings import VcsSettingsModel
345 347
346 348 config = []
347 349
348 350 sa = meta.Session()
349 351 settings_model = VcsSettingsModel(repo=repo, sa=sa)
350 352
351 353 ui_settings = settings_model.get_ui_settings()
352 354
353 355 for setting in ui_settings:
354 356 if setting.active:
355 357 log.debug(
356 358 'settings ui from db: [%s] %s=%s',
357 359 setting.section, setting.key, setting.value)
358 360 config.append((
359 361 safe_str(setting.section), safe_str(setting.key),
360 362 safe_str(setting.value)))
361 363 if setting.key == 'push_ssl':
362 364 # force set push_ssl requirement to False, rhodecode
363 365 # handles that
364 366 config.append((
365 367 safe_str(setting.section), safe_str(setting.key), False))
366 368 if clear_session:
367 369 meta.Session.remove()
368 370
369 371 # TODO: mikhail: probably it makes no sense to re-read hooks information.
370 372 # It's already there and activated/deactivated
371 373 skip_entries = []
372 374 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
373 375 if 'pull' not in enabled_hook_classes:
374 376 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
375 377 if 'push' not in enabled_hook_classes:
376 378 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
377 379 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
378 380 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
379 381
380 382 config = [entry for entry in config if entry[:2] not in skip_entries]
381 383
382 384 return config
383 385
384 386
385 387 def make_db_config(clear_session=True, repo=None):
386 388 """
387 389 Create a :class:`Config` instance based on the values in the database.
388 390 """
389 391 config = Config()
390 392 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
391 393 for section, option, value in config_data:
392 394 config.set(section, option, value)
393 395 return config
394 396
395 397
396 398 def get_enabled_hook_classes(ui_settings):
397 399 """
398 400 Return the enabled hook classes.
399 401
400 402 :param ui_settings: List of ui_settings as returned
401 403 by :meth:`VcsSettingsModel.get_ui_settings`
402 404
403 405 :return: a list with the enabled hook classes. The order is not guaranteed.
404 406 :rtype: list
405 407 """
406 408 enabled_hooks = []
407 409 active_hook_keys = [
408 410 key for section, key, value, active in ui_settings
409 411 if section == 'hooks' and active]
410 412
411 413 hook_names = {
412 414 RhodeCodeUi.HOOK_PUSH: 'push',
413 415 RhodeCodeUi.HOOK_PULL: 'pull',
414 416 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
415 417 }
416 418
417 419 for key in active_hook_keys:
418 420 hook = hook_names.get(key)
419 421 if hook:
420 422 enabled_hooks.append(hook)
421 423
422 424 return enabled_hooks
423 425
424 426
425 427 def set_rhodecode_config(config):
426 428 """
427 429 Updates pyramid config with new settings from database
428 430
429 431 :param config:
430 432 """
431 433 from rhodecode.model.settings import SettingsModel
432 434 app_settings = SettingsModel().get_all_settings()
433 435
434 436 for k, v in app_settings.items():
435 437 config[k] = v
436 438
437 439
438 440 def get_rhodecode_realm():
439 441 """
440 442 Return the rhodecode realm from database.
441 443 """
442 444 from rhodecode.model.settings import SettingsModel
443 445 realm = SettingsModel().get_setting_by_name('realm')
444 446 return safe_str(realm.app_settings_value)
445 447
446 448
447 449 def get_rhodecode_base_path():
448 450 """
449 451 Returns the base path. The base path is the filesystem path which points
450 452 to the repository store.
451 453 """
452 454 from rhodecode.model.settings import SettingsModel
453 455 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
454 456 return safe_str(paths_ui.ui_value)
455 457
456 458
457 459 def map_groups(path):
458 460 """
459 461 Given a full path to a repository, create all nested groups that this
460 462 repo is inside. This function creates parent-child relationships between
461 463 groups and creates default perms for all new groups.
462 464
463 465 :param paths: full path to repository
464 466 """
465 467 from rhodecode.model.repo_group import RepoGroupModel
466 468 sa = meta.Session()
467 469 groups = path.split(Repository.NAME_SEP)
468 470 parent = None
469 471 group = None
470 472
471 473 # last element is repo in nested groups structure
472 474 groups = groups[:-1]
473 475 rgm = RepoGroupModel(sa)
474 476 owner = User.get_first_super_admin()
475 477 for lvl, group_name in enumerate(groups):
476 478 group_name = '/'.join(groups[:lvl] + [group_name])
477 479 group = RepoGroup.get_by_group_name(group_name)
478 480 desc = '%s group' % group_name
479 481
480 482 # skip folders that are now removed repos
481 483 if REMOVED_REPO_PAT.match(group_name):
482 484 break
483 485
484 486 if group is None:
485 487 log.debug('creating group level: %s group_name: %s',
486 488 lvl, group_name)
487 489 group = RepoGroup(group_name, parent)
488 490 group.group_description = desc
489 491 group.user = owner
490 492 sa.add(group)
491 493 perm_obj = rgm._create_default_perms(group)
492 494 sa.add(perm_obj)
493 495 sa.flush()
494 496
495 497 parent = group
496 498 return group
497 499
498 500
499 501 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
500 502 """
501 503 maps all repos given in initial_repo_list, non existing repositories
502 504 are created, if remove_obsolete is True it also checks for db entries
503 505 that are not in initial_repo_list and removes them.
504 506
505 507 :param initial_repo_list: list of repositories found by scanning methods
506 508 :param remove_obsolete: check for obsolete entries in database
507 509 """
508 510 from rhodecode.model.repo import RepoModel
509 511 from rhodecode.model.scm import ScmModel
510 512 from rhodecode.model.repo_group import RepoGroupModel
511 513 from rhodecode.model.settings import SettingsModel
512 514
513 515 sa = meta.Session()
514 516 repo_model = RepoModel()
515 517 user = User.get_first_super_admin()
516 518 added = []
517 519
518 520 # creation defaults
519 521 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
520 522 enable_statistics = defs.get('repo_enable_statistics')
521 523 enable_locking = defs.get('repo_enable_locking')
522 524 enable_downloads = defs.get('repo_enable_downloads')
523 525 private = defs.get('repo_private')
524 526
525 527 for name, repo in initial_repo_list.items():
526 528 group = map_groups(name)
527 529 unicode_name = safe_unicode(name)
528 530 db_repo = repo_model.get_by_repo_name(unicode_name)
529 531 # found repo that is on filesystem not in RhodeCode database
530 532 if not db_repo:
531 533 log.info('repository %s not found, creating now', name)
532 534 added.append(name)
533 535 desc = (repo.description
534 536 if repo.description != 'unknown'
535 537 else '%s repository' % name)
536 538
537 539 db_repo = repo_model._create_repo(
538 540 repo_name=name,
539 541 repo_type=repo.alias,
540 542 description=desc,
541 543 repo_group=getattr(group, 'group_id', None),
542 544 owner=user,
543 545 enable_locking=enable_locking,
544 546 enable_downloads=enable_downloads,
545 547 enable_statistics=enable_statistics,
546 548 private=private,
547 549 state=Repository.STATE_CREATED
548 550 )
549 551 sa.commit()
550 552 # we added that repo just now, and make sure we updated server info
551 553 if db_repo.repo_type == 'git':
552 554 git_repo = db_repo.scm_instance()
553 555 # update repository server-info
554 556 log.debug('Running update server info')
555 557 git_repo._update_server_info()
556 558
557 559 db_repo.update_commit_cache()
558 560
559 561 config = db_repo._config
560 562 config.set('extensions', 'largefiles', '')
561 563 ScmModel().install_hooks(
562 564 db_repo.scm_instance(config=config),
563 565 repo_type=db_repo.repo_type)
564 566
565 567 removed = []
566 568 if remove_obsolete:
567 569 # remove from database those repositories that are not in the filesystem
568 570 for repo in sa.query(Repository).all():
569 571 if repo.repo_name not in initial_repo_list.keys():
570 572 log.debug("Removing non-existing repository found in db `%s`",
571 573 repo.repo_name)
572 574 try:
573 575 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
574 576 sa.commit()
575 577 removed.append(repo.repo_name)
576 578 except Exception:
577 579 # don't hold further removals on error
578 580 log.error(traceback.format_exc())
579 581 sa.rollback()
580 582
581 583 def splitter(full_repo_name):
582 584 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
583 585 gr_name = None
584 586 if len(_parts) == 2:
585 587 gr_name = _parts[0]
586 588 return gr_name
587 589
588 590 initial_repo_group_list = [splitter(x) for x in
589 591 initial_repo_list.keys() if splitter(x)]
590 592
591 593 # remove from database those repository groups that are not in the
592 594 # filesystem due to parent child relationships we need to delete them
593 595 # in a specific order of most nested first
594 596 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
595 597 nested_sort = lambda gr: len(gr.split('/'))
596 598 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
597 599 if group_name not in initial_repo_group_list:
598 600 repo_group = RepoGroup.get_by_group_name(group_name)
599 601 if (repo_group.children.all() or
600 602 not RepoGroupModel().check_exist_filesystem(
601 603 group_name=group_name, exc_on_failure=False)):
602 604 continue
603 605
604 606 log.info(
605 607 'Removing non-existing repository group found in db `%s`',
606 608 group_name)
607 609 try:
608 610 RepoGroupModel(sa).delete(group_name, fs_remove=False)
609 611 sa.commit()
610 612 removed.append(group_name)
611 613 except Exception:
612 614 # don't hold further removals on error
613 615 log.exception(
614 616 'Unable to remove repository group `%s`',
615 617 group_name)
616 618 sa.rollback()
617 619 raise
618 620
619 621 return added, removed
620 622
621 623
622 624 def load_rcextensions(root_path):
623 625 import rhodecode
624 626 from rhodecode.config import conf
625 627
626 628 path = os.path.join(root_path, 'rcextensions', '__init__.py')
627 629 if os.path.isfile(path):
628 630 rcext = create_module('rc', path)
629 631 EXT = rhodecode.EXTENSIONS = rcext
630 632 log.debug('Found rcextensions now loading %s...', rcext)
631 633
632 634 # Additional mappings that are not present in the pygments lexers
633 635 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
634 636
635 637 # auto check if the module is not missing any data, set to default if is
636 638 # this will help autoupdate new feature of rcext module
637 639 #from rhodecode.config import rcextensions
638 640 #for k in dir(rcextensions):
639 641 # if not k.startswith('_') and not hasattr(EXT, k):
640 642 # setattr(EXT, k, getattr(rcextensions, k))
641 643
642 644
643 645 def get_custom_lexer(extension):
644 646 """
645 647 returns a custom lexer if it is defined in rcextensions module, or None
646 648 if there's no custom lexer defined
647 649 """
648 650 import rhodecode
649 651 from pygments import lexers
650 652
651 653 # custom override made by RhodeCode
652 654 if extension in ['mako']:
653 655 return lexers.get_lexer_by_name('html+mako')
654 656
655 657 # check if we didn't define this extension as other lexer
656 658 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
657 659 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
658 660 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
659 661 return lexers.get_lexer_by_name(_lexer_name)
660 662
661 663
662 664 #==============================================================================
663 665 # TEST FUNCTIONS AND CREATORS
664 666 #==============================================================================
665 667 def create_test_index(repo_location, config):
666 668 """
667 669 Makes default test index.
668 670 """
669 671 import rc_testdata
670 672
671 673 rc_testdata.extract_search_index(
672 674 'vcs_search_index', os.path.dirname(config['search.location']))
673 675
674 676
675 677 def create_test_directory(test_path):
676 678 """
677 679 Create test directory if it doesn't exist.
678 680 """
679 681 if not os.path.isdir(test_path):
680 682 log.debug('Creating testdir %s', test_path)
681 683 os.makedirs(test_path)
682 684
683 685
684 686 def create_test_database(test_path, config):
685 687 """
686 688 Makes a fresh database.
687 689 """
688 690 from rhodecode.lib.db_manage import DbManage
689 691
690 692 # PART ONE create db
691 693 dbconf = config['sqlalchemy.db1.url']
692 694 log.debug('making test db %s', dbconf)
693 695
694 696 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
695 697 tests=True, cli_args={'force_ask': True})
696 698 dbmanage.create_tables(override=True)
697 699 dbmanage.set_db_version()
698 700 # for tests dynamically set new root paths based on generated content
699 701 dbmanage.create_settings(dbmanage.config_prompt(test_path))
700 702 dbmanage.create_default_user()
701 703 dbmanage.create_test_admin_and_users()
702 704 dbmanage.create_permissions()
703 705 dbmanage.populate_default_permissions()
704 706 Session().commit()
705 707
706 708
707 709 def create_test_repositories(test_path, config):
708 710 """
709 711 Creates test repositories in the temporary directory. Repositories are
710 712 extracted from archives within the rc_testdata package.
711 713 """
712 714 import rc_testdata
713 715 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
714 716
715 717 log.debug('making test vcs repositories')
716 718
717 719 idx_path = config['search.location']
718 720 data_path = config['cache_dir']
719 721
720 722 # clean index and data
721 723 if idx_path and os.path.exists(idx_path):
722 724 log.debug('remove %s', idx_path)
723 725 shutil.rmtree(idx_path)
724 726
725 727 if data_path and os.path.exists(data_path):
726 728 log.debug('remove %s', data_path)
727 729 shutil.rmtree(data_path)
728 730
729 731 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
730 732 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
731 733
732 734 # Note: Subversion is in the process of being integrated with the system,
733 735 # until we have a properly packed version of the test svn repository, this
734 736 # tries to copy over the repo from a package "rc_testdata"
735 737 svn_repo_path = rc_testdata.get_svn_repo_archive()
736 738 with tarfile.open(svn_repo_path) as tar:
737 739 tar.extractall(jn(test_path, SVN_REPO))
738 740
739 741
740 742 def password_changed(auth_user, session):
741 743 # Never report password change in case of default user or anonymous user.
742 744 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
743 745 return False
744 746
745 747 password_hash = md5(auth_user.password) if auth_user.password else None
746 748 rhodecode_user = session.get('rhodecode_user', {})
747 749 session_password_hash = rhodecode_user.get('password', '')
748 750 return password_hash != session_password_hash
749 751
750 752
751 753 def read_opensource_licenses():
752 754 global _license_cache
753 755
754 756 if not _license_cache:
755 757 licenses = pkg_resources.resource_string(
756 758 'rhodecode', 'config/licenses.json')
757 759 _license_cache = json.loads(licenses)
758 760
759 761 return _license_cache
760 762
761 763
762 764 def generate_platform_uuid():
763 765 """
764 766 Generates platform UUID based on it's name
765 767 """
766 768 import platform
767 769
768 770 try:
769 771 uuid_list = [platform.platform()]
770 772 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
771 773 except Exception as e:
772 774 log.error('Failed to generate host uuid: %s' % e)
773 775 return 'UNDEFINED'
General Comments 0
You need to be logged in to leave comments. Login now