##// END OF EJS Templates
shadow: Use only a single regular expression to generate and match repo/group slugs.
Martin Bornhold -
r901:8555dd6b default
parent child Browse files
Show More
@@ -1,506 +1,505 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 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 logging
28 28 import importlib
29 29 import re
30 30 from functools import wraps
31 31
32 32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 33 from webob.exc import (
34 34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35 35
36 36 import rhodecode
37 37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
40 40 from rhodecode.lib.exceptions import (
41 41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 42 NotAllowedToCreateUserError)
43 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 44 from rhodecode.lib.middleware import appenlight
45 45 from rhodecode.lib.middleware.utils import scm_app
46 46 from rhodecode.lib.utils import (
47 47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
48 48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
49 49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 50 from rhodecode.lib.vcs.backends import base
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import User, Repository, PullRequest
53 53 from rhodecode.model.scm import ScmModel
54 54 from rhodecode.model.pull_request import PullRequestModel
55 55
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 def initialize_generator(factory):
61 61 """
62 62 Initializes the returned generator by draining its first element.
63 63
64 64 This can be used to give a generator an initializer, which is the code
65 65 up to the first yield statement. This decorator enforces that the first
66 66 produced element has the value ``"__init__"`` to make its special
67 67 purpose very explicit in the using code.
68 68 """
69 69
70 70 @wraps(factory)
71 71 def wrapper(*args, **kwargs):
72 72 gen = factory(*args, **kwargs)
73 73 try:
74 74 init = gen.next()
75 75 except StopIteration:
76 76 raise ValueError('Generator must yield at least one element.')
77 77 if init != "__init__":
78 78 raise ValueError('First yielded element must be "__init__".')
79 79 return gen
80 80 return wrapper
81 81
82 82
83 83 class SimpleVCS(object):
84 84 """Common functionality for SCM HTTP handlers."""
85 85
86 86 SCM = 'unknown'
87 87
88 88 acl_repo_name = None
89 89 url_repo_name = None
90 90 vcs_repo_name = None
91 91
92 92 def __init__(self, application, config, registry):
93 93 self.registry = registry
94 94 self.application = application
95 95 self.config = config
96 96 # re-populated by specialized middleware
97 97 self.repo_vcs_config = base.Config()
98 98
99 99 # base path of repo locations
100 100 self.basepath = get_rhodecode_base_path()
101 101 # authenticate this VCS request using authfunc
102 102 auth_ret_code_detection = \
103 103 str2bool(self.config.get('auth_ret_code_detection', False))
104 104 self.authenticate = BasicAuth(
105 105 '', authenticate, registry, config.get('auth_ret_code'),
106 106 auth_ret_code_detection)
107 107 self.ip_addr = '0.0.0.0'
108 108
109 109 def set_repo_names(self, environ):
110 110 """
111 111 This will populate the attributes acl_repo_name, url_repo_name,
112 112 vcs_repo_name and is_shadow_repo on the current instance.
113 113 """
114 114 # TODO: martinb: Move to class or module scope.
115 # TODO: martinb: Check if we have to use re.UNICODE.
116 # TODO: martinb: Check which chars are allowed for repo/group names.
117 # These chars are excluded: '`?=[]\;\'"<>,/~!@#$%^&*()+{}|: '
118 # Code from: rhodecode/lib/utils.py:repo_name_slug()
115 from rhodecode.lib.utils import SLUG_RE
119 116 pr_regex = re.compile(
120 '(?P<base_name>(?:[\w-]+)(?:/[\w-]+)*)/' # repo groups
121 '(?P<repo_name>[\w-]+)' # target repo name
122 '/pull-request/(?P<pr_id>\d+)/repository') # pr suffix
117 '(?P<groups>(?:{slug_pat})(?:/{slug_pat})*)' # repo groups
118 '/(?P<target>{slug_pat})' # target repo
119 '/pull-request/(?P<pr_id>\d+)' # pull request
120 '/repository$' # shadow repo
121 .format(slug_pat=SLUG_RE.pattern))
123 122
124 123 # Get url repo name from environment.
125 124 self.url_repo_name = self._get_repository_name(environ)
126 125
127 126 # Check if this is a request to a shadow repository. In case of a
128 127 # shadow repo set vcs_repo_name to the file system path pointing to the
129 128 # shadow repo. And set acl_repo_name to the pull request target repo
130 129 # because we use the target repo for permission checks. Otherwise all
131 130 # names are equal.
132 131 match = pr_regex.match(self.url_repo_name)
133 132 if match:
134 133 # Get pull request instance.
135 134 match_dict = match.groupdict()
136 135 pr_id = match_dict['pr_id']
137 136 pull_request = PullRequest.get(pr_id)
138 137
139 138 # Get file system path to shadow repository.
140 139 workspace_id = PullRequestModel()._workspace_id(pull_request)
141 140 target_vcs = pull_request.target_repo.scm_instance()
142 141 vcs_repo_name = target_vcs._get_shadow_repository_path(
143 142 workspace_id)
144 143
145 144 # Store names for later usage.
146 145 self.vcs_repo_name = vcs_repo_name
147 146 self.acl_repo_name = pull_request.target_repo.repo_name
148 147 self.is_shadow_repo = True
149 148 else:
150 149 # All names are equal for normal (non shadow) repositories.
151 150 self.acl_repo_name = self.url_repo_name
152 151 self.vcs_repo_name = self.url_repo_name
153 152 self.is_shadow_repo = False
154 153
155 154 @property
156 155 def scm_app(self):
157 156 custom_implementation = self.config.get('vcs.scm_app_implementation')
158 157 if custom_implementation and custom_implementation != 'pyro4':
159 158 log.info(
160 159 "Using custom implementation of scm_app: %s",
161 160 custom_implementation)
162 161 scm_app_impl = importlib.import_module(custom_implementation)
163 162 else:
164 163 scm_app_impl = scm_app
165 164 return scm_app_impl
166 165
167 166 def _get_by_id(self, repo_name):
168 167 """
169 168 Gets a special pattern _<ID> from clone url and tries to replace it
170 169 with a repository_name for support of _<ID> non changeable urls
171 170 """
172 171
173 172 data = repo_name.split('/')
174 173 if len(data) >= 2:
175 174 from rhodecode.model.repo import RepoModel
176 175 by_id_match = RepoModel().get_repo_by_id(repo_name)
177 176 if by_id_match:
178 177 data[1] = by_id_match.repo_name
179 178
180 179 return safe_str('/'.join(data))
181 180
182 181 def _invalidate_cache(self, repo_name):
183 182 """
184 183 Set's cache for this repository for invalidation on next access
185 184
186 185 :param repo_name: full repo name, also a cache key
187 186 """
188 187 ScmModel().mark_for_invalidation(repo_name)
189 188
190 189 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
191 190 db_repo = Repository.get_by_repo_name(repo_name)
192 191 if not db_repo:
193 192 log.debug('Repository `%s` not found inside the database.',
194 193 repo_name)
195 194 return False
196 195
197 196 if db_repo.repo_type != scm_type:
198 197 log.warning(
199 198 'Repository `%s` have incorrect scm_type, expected %s got %s',
200 199 repo_name, db_repo.repo_type, scm_type)
201 200 return False
202 201
203 202 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
204 203
205 204 def valid_and_active_user(self, user):
206 205 """
207 206 Checks if that user is not empty, and if it's actually object it checks
208 207 if he's active.
209 208
210 209 :param user: user object or None
211 210 :return: boolean
212 211 """
213 212 if user is None:
214 213 return False
215 214
216 215 elif user.active:
217 216 return True
218 217
219 218 return False
220 219
221 220 def _check_permission(self, action, user, repo_name, ip_addr=None):
222 221 """
223 222 Checks permissions using action (push/pull) user and repository
224 223 name
225 224
226 225 :param action: push or pull action
227 226 :param user: user instance
228 227 :param repo_name: repository name
229 228 """
230 229 # check IP
231 230 inherit = user.inherit_default_permissions
232 231 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
233 232 inherit_from_default=inherit)
234 233 if ip_allowed:
235 234 log.info('Access for IP:%s allowed', ip_addr)
236 235 else:
237 236 return False
238 237
239 238 if action == 'push':
240 239 if not HasPermissionAnyMiddleware('repository.write',
241 240 'repository.admin')(user,
242 241 repo_name):
243 242 return False
244 243
245 244 else:
246 245 # any other action need at least read permission
247 246 if not HasPermissionAnyMiddleware('repository.read',
248 247 'repository.write',
249 248 'repository.admin')(user,
250 249 repo_name):
251 250 return False
252 251
253 252 return True
254 253
255 254 def _check_ssl(self, environ, start_response):
256 255 """
257 256 Checks the SSL check flag and returns False if SSL is not present
258 257 and required True otherwise
259 258 """
260 259 org_proto = environ['wsgi._org_proto']
261 260 # check if we have SSL required ! if not it's a bad request !
262 261 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
263 262 if require_ssl and org_proto == 'http':
264 263 log.debug('proto is %s and SSL is required BAD REQUEST !',
265 264 org_proto)
266 265 return False
267 266 return True
268 267
269 268 def __call__(self, environ, start_response):
270 269 try:
271 270 return self._handle_request(environ, start_response)
272 271 except Exception:
273 272 log.exception("Exception while handling request")
274 273 appenlight.track_exception(environ)
275 274 return HTTPInternalServerError()(environ, start_response)
276 275 finally:
277 276 meta.Session.remove()
278 277
279 278 def _handle_request(self, environ, start_response):
280 279
281 280 if not self._check_ssl(environ, start_response):
282 281 reason = ('SSL required, while RhodeCode was unable '
283 282 'to detect this as SSL request')
284 283 log.debug('User not allowed to proceed, %s', reason)
285 284 return HTTPNotAcceptable(reason)(environ, start_response)
286 285
287 286 if not self.url_repo_name:
288 287 log.warning('Repository name is empty: %s', self.url_repo_name)
289 288 # failed to get repo name, we fail now
290 289 return HTTPNotFound()(environ, start_response)
291 290 log.debug('Extracted repo name is %s', self.url_repo_name)
292 291
293 292 ip_addr = get_ip_addr(environ)
294 293 username = None
295 294
296 295 # skip passing error to error controller
297 296 environ['pylons.status_code_redirect'] = True
298 297
299 298 # ======================================================================
300 299 # GET ACTION PULL or PUSH
301 300 # ======================================================================
302 301 action = self._get_action(environ)
303 302
304 303 # ======================================================================
305 304 # Check if this is a request to a shadow repository of a pull request.
306 305 # In this case only pull action is allowed.
307 306 # ======================================================================
308 307 if self.is_shadow_repo and action != 'pull':
309 308 reason = 'Only pull action is allowed for shadow repositories.'
310 309 log.debug('User not allowed to proceed, %s', reason)
311 310 return HTTPNotAcceptable(reason)(environ, start_response)
312 311
313 312 # ======================================================================
314 313 # CHECK ANONYMOUS PERMISSION
315 314 # ======================================================================
316 315 if action in ['pull', 'push']:
317 316 anonymous_user = User.get_default_user()
318 317 username = anonymous_user.username
319 318 if anonymous_user.active:
320 319 # ONLY check permissions if the user is activated
321 320 anonymous_perm = self._check_permission(
322 321 action, anonymous_user, self.acl_repo_name, ip_addr)
323 322 else:
324 323 anonymous_perm = False
325 324
326 325 if not anonymous_user.active or not anonymous_perm:
327 326 if not anonymous_user.active:
328 327 log.debug('Anonymous access is disabled, running '
329 328 'authentication')
330 329
331 330 if not anonymous_perm:
332 331 log.debug('Not enough credentials to access this '
333 332 'repository as anonymous user')
334 333
335 334 username = None
336 335 # ==============================================================
337 336 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
338 337 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
339 338 # ==============================================================
340 339
341 340 # try to auth based on environ, container auth methods
342 341 log.debug('Running PRE-AUTH for container based authentication')
343 342 pre_auth = authenticate(
344 343 '', '', environ, VCS_TYPE, registry=self.registry)
345 344 if pre_auth and pre_auth.get('username'):
346 345 username = pre_auth['username']
347 346 log.debug('PRE-AUTH got %s as username', username)
348 347
349 348 # If not authenticated by the container, running basic auth
350 349 if not username:
351 350 self.authenticate.realm = get_rhodecode_realm()
352 351
353 352 try:
354 353 result = self.authenticate(environ)
355 354 except (UserCreationError, NotAllowedToCreateUserError) as e:
356 355 log.error(e)
357 356 reason = safe_str(e)
358 357 return HTTPNotAcceptable(reason)(environ, start_response)
359 358
360 359 if isinstance(result, str):
361 360 AUTH_TYPE.update(environ, 'basic')
362 361 REMOTE_USER.update(environ, result)
363 362 username = result
364 363 else:
365 364 return result.wsgi_application(environ, start_response)
366 365
367 366 # ==============================================================
368 367 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
369 368 # ==============================================================
370 369 user = User.get_by_username(username)
371 370 if not self.valid_and_active_user(user):
372 371 return HTTPForbidden()(environ, start_response)
373 372 username = user.username
374 373 user.update_lastactivity()
375 374 meta.Session().commit()
376 375
377 376 # check user attributes for password change flag
378 377 user_obj = user
379 378 if user_obj and user_obj.username != User.DEFAULT_USER and \
380 379 user_obj.user_data.get('force_password_change'):
381 380 reason = 'password change required'
382 381 log.debug('User not allowed to authenticate, %s', reason)
383 382 return HTTPNotAcceptable(reason)(environ, start_response)
384 383
385 384 # check permissions for this repository
386 385 perm = self._check_permission(
387 386 action, user, self.acl_repo_name, ip_addr)
388 387 if not perm:
389 388 return HTTPForbidden()(environ, start_response)
390 389
391 390 # extras are injected into UI object and later available
392 391 # in hooks executed by rhodecode
393 392 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
394 393 extras = vcs_operation_context(
395 394 environ, repo_name=self.acl_repo_name, username=username,
396 395 action=action, scm=self.SCM, check_locking=check_locking,
397 396 is_shadow_repo=self.is_shadow_repo
398 397 )
399 398
400 399 # ======================================================================
401 400 # REQUEST HANDLING
402 401 # ======================================================================
403 402 str_repo_name = safe_str(self.url_repo_name)
404 403 repo_path = os.path.join(
405 404 safe_str(self.basepath), safe_str(self.vcs_repo_name))
406 405 log.debug('Repository path is %s', repo_path)
407 406
408 407 fix_PATH()
409 408
410 409 log.info(
411 410 '%s action on %s repo "%s" by "%s" from %s',
412 411 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
413 412
414 413 return self._generate_vcs_response(
415 414 environ, start_response, repo_path, self.url_repo_name, extras, action)
416 415
417 416 @initialize_generator
418 417 def _generate_vcs_response(
419 418 self, environ, start_response, repo_path, repo_name, extras,
420 419 action):
421 420 """
422 421 Returns a generator for the response content.
423 422
424 423 This method is implemented as a generator, so that it can trigger
425 424 the cache validation after all content sent back to the client. It
426 425 also handles the locking exceptions which will be triggered when
427 426 the first chunk is produced by the underlying WSGI application.
428 427 """
429 428 callback_daemon, extras = self._prepare_callback_daemon(extras)
430 429 config = self._create_config(extras, self.acl_repo_name)
431 430 log.debug('HOOKS extras is %s', extras)
432 431 app = self._create_wsgi_app(repo_path, repo_name, config)
433 432
434 433 try:
435 434 with callback_daemon:
436 435 try:
437 436 response = app(environ, start_response)
438 437 finally:
439 438 # This statement works together with the decorator
440 439 # "initialize_generator" above. The decorator ensures that
441 440 # we hit the first yield statement before the generator is
442 441 # returned back to the WSGI server. This is needed to
443 442 # ensure that the call to "app" above triggers the
444 443 # needed callback to "start_response" before the
445 444 # generator is actually used.
446 445 yield "__init__"
447 446
448 447 for chunk in response:
449 448 yield chunk
450 449 except Exception as exc:
451 450 # TODO: johbo: Improve "translating" back the exception.
452 451 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
453 452 exc = HTTPLockedRC(*exc.args)
454 453 _code = rhodecode.CONFIG.get('lock_ret_code')
455 454 log.debug('Repository LOCKED ret code %s!', (_code,))
456 455 elif getattr(exc, '_vcs_kind', None) == 'requirement':
457 456 log.debug(
458 457 'Repository requires features unknown to this Mercurial')
459 458 exc = HTTPRequirementError(*exc.args)
460 459 else:
461 460 raise
462 461
463 462 for chunk in exc(environ, start_response):
464 463 yield chunk
465 464 finally:
466 465 # invalidate cache on push
467 466 try:
468 467 if action == 'push':
469 468 self._invalidate_cache(repo_name)
470 469 finally:
471 470 meta.Session.remove()
472 471
473 472 def _get_repository_name(self, environ):
474 473 """Get repository name out of the environmnent
475 474
476 475 :param environ: WSGI environment
477 476 """
478 477 raise NotImplementedError()
479 478
480 479 def _get_action(self, environ):
481 480 """Map request commands into a pull or push command.
482 481
483 482 :param environ: WSGI environment
484 483 """
485 484 raise NotImplementedError()
486 485
487 486 def _create_wsgi_app(self, repo_path, repo_name, config):
488 487 """Return the WSGI app that will finally handle the request."""
489 488 raise NotImplementedError()
490 489
491 490 def _create_config(self, extras, repo_name):
492 491 """Create a Pyro safe config representation."""
493 492 raise NotImplementedError()
494 493
495 494 def _prepare_callback_daemon(self, extras):
496 495 return prepare_callback_daemon(
497 496 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
498 497 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
499 498
500 499
501 500 def _should_check_locking(query_string):
502 501 # this is kind of hacky, but due to how mercurial handles client-server
503 502 # server see all operation on commit; bookmarks, phases and
504 503 # obsolescence marker in different transaction, we don't want to check
505 504 # locking on those
506 505 return query_string not in ['cmd=listkeys']
@@ -1,1011 +1,998 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 from os.path import join as jn
37 37
38 38 import paste
39 39 import pkg_resources
40 40 from paste.script.command import Command, BadCommand
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
45 45 from rhodecode.lib.fakemod import create_module
46 46 from rhodecode.lib.vcs.backends.base import Config
47 47 from rhodecode.lib.vcs.exceptions import VCSError
48 48 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
49 49 from rhodecode.lib.utils2 import (
50 50 safe_str, safe_unicode, get_current_rhodecode_user, md5)
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import (
53 53 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
54 54 from rhodecode.model.meta import Session
55 55
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
60 60
61 _license_cache = None
62
63
64 def recursive_replace(str_, replace=' '):
65 """
66 Recursive replace of given sign to just one instance
67
68 :param str_: given string
69 :param replace: char to find and replace multiple instances
61 # String of characters which are not allowed in repo/group slugs.
62 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
63 # Regex that matches forbidden characters in repo/group slugs.
64 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
65 # Regex that matches allowed characters in repo/group slugs.
66 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
67 # Regex that matches whole repo/group slugs.
68 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
70 69
71 Examples::
72 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
73 'Mighty-Mighty-Bo-sstones'
74 """
75
76 if str_.find(replace * 2) == -1:
77 return str_
78 else:
79 str_ = str_.replace(replace * 2, replace)
80 return recursive_replace(str_, replace)
70 _license_cache = None
81 71
82 72
83 73 def repo_name_slug(value):
84 74 """
85 75 Return slug of name of repository
86 76 This function is called on each creation/modification
87 77 of repository to prevent bad names in repo
88 78 """
79 replacement_char = '-'
89 80
90 81 slug = remove_formatting(value)
91 slug = strip_tags(slug)
92
93 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
94 slug = slug.replace(c, '-')
95 slug = recursive_replace(slug, '-')
96 slug = collapse(slug, '-')
82 slug = SLUG_BAD_CHAR_RE.sub(replacement_char, slug)
83 slug = collapse(slug, replacement_char)
97 84 return slug
98 85
99 86
100 87 #==============================================================================
101 88 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
102 89 #==============================================================================
103 90 def get_repo_slug(request):
104 91 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 92 if _repo:
106 93 _repo = _repo.rstrip('/')
107 94 return _repo
108 95
109 96
110 97 def get_repo_group_slug(request):
111 98 _group = request.environ['pylons.routes_dict'].get('group_name')
112 99 if _group:
113 100 _group = _group.rstrip('/')
114 101 return _group
115 102
116 103
117 104 def get_user_group_slug(request):
118 105 _group = request.environ['pylons.routes_dict'].get('user_group_id')
119 106 try:
120 107 _group = UserGroup.get(_group)
121 108 if _group:
122 109 _group = _group.users_group_name
123 110 except Exception:
124 111 log.debug(traceback.format_exc())
125 112 #catch all failures here
126 113 pass
127 114
128 115 return _group
129 116
130 117
131 118 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
132 119 """
133 120 Action logger for various actions made by users
134 121
135 122 :param user: user that made this action, can be a unique username string or
136 123 object containing user_id attribute
137 124 :param action: action to log, should be on of predefined unique actions for
138 125 easy translations
139 126 :param repo: string name of repository or object containing repo_id,
140 127 that action was made on
141 128 :param ipaddr: optional ip address from what the action was made
142 129 :param sa: optional sqlalchemy session
143 130
144 131 """
145 132
146 133 if not sa:
147 134 sa = meta.Session()
148 135 # if we don't get explicit IP address try to get one from registered user
149 136 # in tmpl context var
150 137 if not ipaddr:
151 138 ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '')
152 139
153 140 try:
154 141 if getattr(user, 'user_id', None):
155 142 user_obj = User.get(user.user_id)
156 143 elif isinstance(user, basestring):
157 144 user_obj = User.get_by_username(user)
158 145 else:
159 146 raise Exception('You have to provide a user object or a username')
160 147
161 148 if getattr(repo, 'repo_id', None):
162 149 repo_obj = Repository.get(repo.repo_id)
163 150 repo_name = repo_obj.repo_name
164 151 elif isinstance(repo, basestring):
165 152 repo_name = repo.lstrip('/')
166 153 repo_obj = Repository.get_by_repo_name(repo_name)
167 154 else:
168 155 repo_obj = None
169 156 repo_name = ''
170 157
171 158 user_log = UserLog()
172 159 user_log.user_id = user_obj.user_id
173 160 user_log.username = user_obj.username
174 161 action = safe_unicode(action)
175 162 user_log.action = action[:1200000]
176 163
177 164 user_log.repository = repo_obj
178 165 user_log.repository_name = repo_name
179 166
180 167 user_log.action_date = datetime.datetime.now()
181 168 user_log.user_ip = ipaddr
182 169 sa.add(user_log)
183 170
184 171 log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s',
185 172 action, safe_unicode(repo), user_obj, ipaddr)
186 173 if commit:
187 174 sa.commit()
188 175 except Exception:
189 176 log.error(traceback.format_exc())
190 177 raise
191 178
192 179
193 180 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
194 181 """
195 182 Scans given path for repos and return (name,(type,path)) tuple
196 183
197 184 :param path: path to scan for repositories
198 185 :param recursive: recursive search and return names with subdirs in front
199 186 """
200 187
201 188 # remove ending slash for better results
202 189 path = path.rstrip(os.sep)
203 190 log.debug('now scanning in %s location recursive:%s...', path, recursive)
204 191
205 192 def _get_repos(p):
206 193 dirpaths = _get_dirpaths(p)
207 194 if not _is_dir_writable(p):
208 195 log.warning('repo path without write access: %s', p)
209 196
210 197 for dirpath in dirpaths:
211 198 if os.path.isfile(os.path.join(p, dirpath)):
212 199 continue
213 200 cur_path = os.path.join(p, dirpath)
214 201
215 202 # skip removed repos
216 203 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
217 204 continue
218 205
219 206 #skip .<somethin> dirs
220 207 if dirpath.startswith('.'):
221 208 continue
222 209
223 210 try:
224 211 scm_info = get_scm(cur_path)
225 212 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
226 213 except VCSError:
227 214 if not recursive:
228 215 continue
229 216 #check if this dir containts other repos for recursive scan
230 217 rec_path = os.path.join(p, dirpath)
231 218 if os.path.isdir(rec_path):
232 219 for inner_scm in _get_repos(rec_path):
233 220 yield inner_scm
234 221
235 222 return _get_repos(path)
236 223
237 224
238 225 def _get_dirpaths(p):
239 226 try:
240 227 # OS-independable way of checking if we have at least read-only
241 228 # access or not.
242 229 dirpaths = os.listdir(p)
243 230 except OSError:
244 231 log.warning('ignoring repo path without read access: %s', p)
245 232 return []
246 233
247 234 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
248 235 # decode paths and suddenly returns unicode objects itself. The items it
249 236 # cannot decode are returned as strings and cause issues.
250 237 #
251 238 # Those paths are ignored here until a solid solution for path handling has
252 239 # been built.
253 240 expected_type = type(p)
254 241
255 242 def _has_correct_type(item):
256 243 if type(item) is not expected_type:
257 244 log.error(
258 245 u"Ignoring path %s since it cannot be decoded into unicode.",
259 246 # Using "repr" to make sure that we see the byte value in case
260 247 # of support.
261 248 repr(item))
262 249 return False
263 250 return True
264 251
265 252 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
266 253
267 254 return dirpaths
268 255
269 256
270 257 def _is_dir_writable(path):
271 258 """
272 259 Probe if `path` is writable.
273 260
274 261 Due to trouble on Cygwin / Windows, this is actually probing if it is
275 262 possible to create a file inside of `path`, stat does not produce reliable
276 263 results in this case.
277 264 """
278 265 try:
279 266 with tempfile.TemporaryFile(dir=path):
280 267 pass
281 268 except OSError:
282 269 return False
283 270 return True
284 271
285 272
286 273 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
287 274 """
288 275 Returns True if given path is a valid repository False otherwise.
289 276 If expect_scm param is given also, compare if given scm is the same
290 277 as expected from scm parameter. If explicit_scm is given don't try to
291 278 detect the scm, just use the given one to check if repo is valid
292 279
293 280 :param repo_name:
294 281 :param base_path:
295 282 :param expect_scm:
296 283 :param explicit_scm:
297 284
298 285 :return True: if given path is a valid repository
299 286 """
300 287 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
301 288 log.debug('Checking if `%s` is a valid path for repository', repo_name)
302 289
303 290 try:
304 291 if explicit_scm:
305 292 detected_scms = [get_scm_backend(explicit_scm)]
306 293 else:
307 294 detected_scms = get_scm(full_path)
308 295
309 296 if expect_scm:
310 297 return detected_scms[0] == expect_scm
311 298 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
312 299 return True
313 300 except VCSError:
314 301 log.debug('path: %s is not a valid repo !', full_path)
315 302 return False
316 303
317 304
318 305 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
319 306 """
320 307 Returns True if given path is a repository group, False otherwise
321 308
322 309 :param repo_name:
323 310 :param base_path:
324 311 """
325 312 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
326 313 log.debug('Checking if `%s` is a valid path for repository group',
327 314 repo_group_name)
328 315
329 316 # check if it's not a repo
330 317 if is_valid_repo(repo_group_name, base_path):
331 318 log.debug('Repo called %s exist, it is not a valid '
332 319 'repo group' % repo_group_name)
333 320 return False
334 321
335 322 try:
336 323 # we need to check bare git repos at higher level
337 324 # since we might match branches/hooks/info/objects or possible
338 325 # other things inside bare git repo
339 326 scm_ = get_scm(os.path.dirname(full_path))
340 327 log.debug('path: %s is a vcs object:%s, not valid '
341 328 'repo group' % (full_path, scm_))
342 329 return False
343 330 except VCSError:
344 331 pass
345 332
346 333 # check if it's a valid path
347 334 if skip_path_check or os.path.isdir(full_path):
348 335 log.debug('path: %s is a valid repo group !', full_path)
349 336 return True
350 337
351 338 log.debug('path: %s is not a valid repo group !', full_path)
352 339 return False
353 340
354 341
355 342 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
356 343 while True:
357 344 ok = raw_input(prompt)
358 345 if ok.lower() in ('y', 'ye', 'yes'):
359 346 return True
360 347 if ok.lower() in ('n', 'no', 'nop', 'nope'):
361 348 return False
362 349 retries = retries - 1
363 350 if retries < 0:
364 351 raise IOError
365 352 print(complaint)
366 353
367 354 # propagated from mercurial documentation
368 355 ui_sections = [
369 356 'alias', 'auth',
370 357 'decode/encode', 'defaults',
371 358 'diff', 'email',
372 359 'extensions', 'format',
373 360 'merge-patterns', 'merge-tools',
374 361 'hooks', 'http_proxy',
375 362 'smtp', 'patch',
376 363 'paths', 'profiling',
377 364 'server', 'trusted',
378 365 'ui', 'web', ]
379 366
380 367
381 368 def config_data_from_db(clear_session=True, repo=None):
382 369 """
383 370 Read the configuration data from the database and return configuration
384 371 tuples.
385 372 """
386 373 from rhodecode.model.settings import VcsSettingsModel
387 374
388 375 config = []
389 376
390 377 sa = meta.Session()
391 378 settings_model = VcsSettingsModel(repo=repo, sa=sa)
392 379
393 380 ui_settings = settings_model.get_ui_settings()
394 381
395 382 for setting in ui_settings:
396 383 if setting.active:
397 384 log.debug(
398 385 'settings ui from db: [%s] %s=%s',
399 386 setting.section, setting.key, setting.value)
400 387 config.append((
401 388 safe_str(setting.section), safe_str(setting.key),
402 389 safe_str(setting.value)))
403 390 if setting.key == 'push_ssl':
404 391 # force set push_ssl requirement to False, rhodecode
405 392 # handles that
406 393 config.append((
407 394 safe_str(setting.section), safe_str(setting.key), False))
408 395 if clear_session:
409 396 meta.Session.remove()
410 397
411 398 # TODO: mikhail: probably it makes no sense to re-read hooks information.
412 399 # It's already there and activated/deactivated
413 400 skip_entries = []
414 401 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
415 402 if 'pull' not in enabled_hook_classes:
416 403 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
417 404 if 'push' not in enabled_hook_classes:
418 405 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
419 406
420 407 config = [entry for entry in config if entry[:2] not in skip_entries]
421 408
422 409 return config
423 410
424 411
425 412 def make_db_config(clear_session=True, repo=None):
426 413 """
427 414 Create a :class:`Config` instance based on the values in the database.
428 415 """
429 416 config = Config()
430 417 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
431 418 for section, option, value in config_data:
432 419 config.set(section, option, value)
433 420 return config
434 421
435 422
436 423 def get_enabled_hook_classes(ui_settings):
437 424 """
438 425 Return the enabled hook classes.
439 426
440 427 :param ui_settings: List of ui_settings as returned
441 428 by :meth:`VcsSettingsModel.get_ui_settings`
442 429
443 430 :return: a list with the enabled hook classes. The order is not guaranteed.
444 431 :rtype: list
445 432 """
446 433 enabled_hooks = []
447 434 active_hook_keys = [
448 435 key for section, key, value, active in ui_settings
449 436 if section == 'hooks' and active]
450 437
451 438 hook_names = {
452 439 RhodeCodeUi.HOOK_PUSH: 'push',
453 440 RhodeCodeUi.HOOK_PULL: 'pull',
454 441 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
455 442 }
456 443
457 444 for key in active_hook_keys:
458 445 hook = hook_names.get(key)
459 446 if hook:
460 447 enabled_hooks.append(hook)
461 448
462 449 return enabled_hooks
463 450
464 451
465 452 def set_rhodecode_config(config):
466 453 """
467 454 Updates pylons config with new settings from database
468 455
469 456 :param config:
470 457 """
471 458 from rhodecode.model.settings import SettingsModel
472 459 app_settings = SettingsModel().get_all_settings()
473 460
474 461 for k, v in app_settings.items():
475 462 config[k] = v
476 463
477 464
478 465 def get_rhodecode_realm():
479 466 """
480 467 Return the rhodecode realm from database.
481 468 """
482 469 from rhodecode.model.settings import SettingsModel
483 470 realm = SettingsModel().get_setting_by_name('realm')
484 471 return safe_str(realm.app_settings_value)
485 472
486 473
487 474 def get_rhodecode_base_path():
488 475 """
489 476 Returns the base path. The base path is the filesystem path which points
490 477 to the repository store.
491 478 """
492 479 from rhodecode.model.settings import SettingsModel
493 480 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
494 481 return safe_str(paths_ui.ui_value)
495 482
496 483
497 484 def map_groups(path):
498 485 """
499 486 Given a full path to a repository, create all nested groups that this
500 487 repo is inside. This function creates parent-child relationships between
501 488 groups and creates default perms for all new groups.
502 489
503 490 :param paths: full path to repository
504 491 """
505 492 from rhodecode.model.repo_group import RepoGroupModel
506 493 sa = meta.Session()
507 494 groups = path.split(Repository.NAME_SEP)
508 495 parent = None
509 496 group = None
510 497
511 498 # last element is repo in nested groups structure
512 499 groups = groups[:-1]
513 500 rgm = RepoGroupModel(sa)
514 501 owner = User.get_first_super_admin()
515 502 for lvl, group_name in enumerate(groups):
516 503 group_name = '/'.join(groups[:lvl] + [group_name])
517 504 group = RepoGroup.get_by_group_name(group_name)
518 505 desc = '%s group' % group_name
519 506
520 507 # skip folders that are now removed repos
521 508 if REMOVED_REPO_PAT.match(group_name):
522 509 break
523 510
524 511 if group is None:
525 512 log.debug('creating group level: %s group_name: %s',
526 513 lvl, group_name)
527 514 group = RepoGroup(group_name, parent)
528 515 group.group_description = desc
529 516 group.user = owner
530 517 sa.add(group)
531 518 perm_obj = rgm._create_default_perms(group)
532 519 sa.add(perm_obj)
533 520 sa.flush()
534 521
535 522 parent = group
536 523 return group
537 524
538 525
539 526 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
540 527 """
541 528 maps all repos given in initial_repo_list, non existing repositories
542 529 are created, if remove_obsolete is True it also checks for db entries
543 530 that are not in initial_repo_list and removes them.
544 531
545 532 :param initial_repo_list: list of repositories found by scanning methods
546 533 :param remove_obsolete: check for obsolete entries in database
547 534 """
548 535 from rhodecode.model.repo import RepoModel
549 536 from rhodecode.model.scm import ScmModel
550 537 from rhodecode.model.repo_group import RepoGroupModel
551 538 from rhodecode.model.settings import SettingsModel
552 539
553 540 sa = meta.Session()
554 541 repo_model = RepoModel()
555 542 user = User.get_first_super_admin()
556 543 added = []
557 544
558 545 # creation defaults
559 546 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
560 547 enable_statistics = defs.get('repo_enable_statistics')
561 548 enable_locking = defs.get('repo_enable_locking')
562 549 enable_downloads = defs.get('repo_enable_downloads')
563 550 private = defs.get('repo_private')
564 551
565 552 for name, repo in initial_repo_list.items():
566 553 group = map_groups(name)
567 554 unicode_name = safe_unicode(name)
568 555 db_repo = repo_model.get_by_repo_name(unicode_name)
569 556 # found repo that is on filesystem not in RhodeCode database
570 557 if not db_repo:
571 558 log.info('repository %s not found, creating now', name)
572 559 added.append(name)
573 560 desc = (repo.description
574 561 if repo.description != 'unknown'
575 562 else '%s repository' % name)
576 563
577 564 db_repo = repo_model._create_repo(
578 565 repo_name=name,
579 566 repo_type=repo.alias,
580 567 description=desc,
581 568 repo_group=getattr(group, 'group_id', None),
582 569 owner=user,
583 570 enable_locking=enable_locking,
584 571 enable_downloads=enable_downloads,
585 572 enable_statistics=enable_statistics,
586 573 private=private,
587 574 state=Repository.STATE_CREATED
588 575 )
589 576 sa.commit()
590 577 # we added that repo just now, and make sure we updated server info
591 578 if db_repo.repo_type == 'git':
592 579 git_repo = db_repo.scm_instance()
593 580 # update repository server-info
594 581 log.debug('Running update server info')
595 582 git_repo._update_server_info()
596 583
597 584 db_repo.update_commit_cache()
598 585
599 586 config = db_repo._config
600 587 config.set('extensions', 'largefiles', '')
601 588 ScmModel().install_hooks(
602 589 db_repo.scm_instance(config=config),
603 590 repo_type=db_repo.repo_type)
604 591
605 592 removed = []
606 593 if remove_obsolete:
607 594 # remove from database those repositories that are not in the filesystem
608 595 for repo in sa.query(Repository).all():
609 596 if repo.repo_name not in initial_repo_list.keys():
610 597 log.debug("Removing non-existing repository found in db `%s`",
611 598 repo.repo_name)
612 599 try:
613 600 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
614 601 sa.commit()
615 602 removed.append(repo.repo_name)
616 603 except Exception:
617 604 # don't hold further removals on error
618 605 log.error(traceback.format_exc())
619 606 sa.rollback()
620 607
621 608 def splitter(full_repo_name):
622 609 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
623 610 gr_name = None
624 611 if len(_parts) == 2:
625 612 gr_name = _parts[0]
626 613 return gr_name
627 614
628 615 initial_repo_group_list = [splitter(x) for x in
629 616 initial_repo_list.keys() if splitter(x)]
630 617
631 618 # remove from database those repository groups that are not in the
632 619 # filesystem due to parent child relationships we need to delete them
633 620 # in a specific order of most nested first
634 621 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
635 622 nested_sort = lambda gr: len(gr.split('/'))
636 623 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
637 624 if group_name not in initial_repo_group_list:
638 625 repo_group = RepoGroup.get_by_group_name(group_name)
639 626 if (repo_group.children.all() or
640 627 not RepoGroupModel().check_exist_filesystem(
641 628 group_name=group_name, exc_on_failure=False)):
642 629 continue
643 630
644 631 log.info(
645 632 'Removing non-existing repository group found in db `%s`',
646 633 group_name)
647 634 try:
648 635 RepoGroupModel(sa).delete(group_name, fs_remove=False)
649 636 sa.commit()
650 637 removed.append(group_name)
651 638 except Exception:
652 639 # don't hold further removals on error
653 640 log.exception(
654 641 'Unable to remove repository group `%s`',
655 642 group_name)
656 643 sa.rollback()
657 644 raise
658 645
659 646 return added, removed
660 647
661 648
662 649 def get_default_cache_settings(settings):
663 650 cache_settings = {}
664 651 for key in settings.keys():
665 652 for prefix in ['beaker.cache.', 'cache.']:
666 653 if key.startswith(prefix):
667 654 name = key.split(prefix)[1].strip()
668 655 cache_settings[name] = settings[key].strip()
669 656 return cache_settings
670 657
671 658
672 659 # set cache regions for beaker so celery can utilise it
673 660 def add_cache(settings):
674 661 from rhodecode.lib import caches
675 662 cache_settings = {'regions': None}
676 663 # main cache settings used as default ...
677 664 cache_settings.update(get_default_cache_settings(settings))
678 665
679 666 if cache_settings['regions']:
680 667 for region in cache_settings['regions'].split(','):
681 668 region = region.strip()
682 669 region_settings = {}
683 670 for key, value in cache_settings.items():
684 671 if key.startswith(region):
685 672 region_settings[key.split('.')[1]] = value
686 673
687 674 caches.configure_cache_region(
688 675 region, region_settings, cache_settings)
689 676
690 677
691 678 def load_rcextensions(root_path):
692 679 import rhodecode
693 680 from rhodecode.config import conf
694 681
695 682 path = os.path.join(root_path, 'rcextensions', '__init__.py')
696 683 if os.path.isfile(path):
697 684 rcext = create_module('rc', path)
698 685 EXT = rhodecode.EXTENSIONS = rcext
699 686 log.debug('Found rcextensions now loading %s...', rcext)
700 687
701 688 # Additional mappings that are not present in the pygments lexers
702 689 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
703 690
704 691 # auto check if the module is not missing any data, set to default if is
705 692 # this will help autoupdate new feature of rcext module
706 693 #from rhodecode.config import rcextensions
707 694 #for k in dir(rcextensions):
708 695 # if not k.startswith('_') and not hasattr(EXT, k):
709 696 # setattr(EXT, k, getattr(rcextensions, k))
710 697
711 698
712 699 def get_custom_lexer(extension):
713 700 """
714 701 returns a custom lexer if it is defined in rcextensions module, or None
715 702 if there's no custom lexer defined
716 703 """
717 704 import rhodecode
718 705 from pygments import lexers
719 706 # check if we didn't define this extension as other lexer
720 707 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
721 708 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
722 709 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
723 710 return lexers.get_lexer_by_name(_lexer_name)
724 711
725 712
726 713 #==============================================================================
727 714 # TEST FUNCTIONS AND CREATORS
728 715 #==============================================================================
729 716 def create_test_index(repo_location, config):
730 717 """
731 718 Makes default test index.
732 719 """
733 720 import rc_testdata
734 721
735 722 rc_testdata.extract_search_index(
736 723 'vcs_search_index', os.path.dirname(config['search.location']))
737 724
738 725
739 726 def create_test_directory(test_path):
740 727 """
741 728 Create test directory if it doesn't exist.
742 729 """
743 730 if not os.path.isdir(test_path):
744 731 log.debug('Creating testdir %s', test_path)
745 732 os.makedirs(test_path)
746 733
747 734
748 735 def create_test_database(test_path, config):
749 736 """
750 737 Makes a fresh database.
751 738 """
752 739 from rhodecode.lib.db_manage import DbManage
753 740
754 741 # PART ONE create db
755 742 dbconf = config['sqlalchemy.db1.url']
756 743 log.debug('making test db %s', dbconf)
757 744
758 745 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
759 746 tests=True, cli_args={'force_ask': True})
760 747 dbmanage.create_tables(override=True)
761 748 dbmanage.set_db_version()
762 749 # for tests dynamically set new root paths based on generated content
763 750 dbmanage.create_settings(dbmanage.config_prompt(test_path))
764 751 dbmanage.create_default_user()
765 752 dbmanage.create_test_admin_and_users()
766 753 dbmanage.create_permissions()
767 754 dbmanage.populate_default_permissions()
768 755 Session().commit()
769 756
770 757
771 758 def create_test_repositories(test_path, config):
772 759 """
773 760 Creates test repositories in the temporary directory. Repositories are
774 761 extracted from archives within the rc_testdata package.
775 762 """
776 763 import rc_testdata
777 764 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
778 765
779 766 log.debug('making test vcs repositories')
780 767
781 768 idx_path = config['search.location']
782 769 data_path = config['cache_dir']
783 770
784 771 # clean index and data
785 772 if idx_path and os.path.exists(idx_path):
786 773 log.debug('remove %s', idx_path)
787 774 shutil.rmtree(idx_path)
788 775
789 776 if data_path and os.path.exists(data_path):
790 777 log.debug('remove %s', data_path)
791 778 shutil.rmtree(data_path)
792 779
793 780 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
794 781 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
795 782
796 783 # Note: Subversion is in the process of being integrated with the system,
797 784 # until we have a properly packed version of the test svn repository, this
798 785 # tries to copy over the repo from a package "rc_testdata"
799 786 svn_repo_path = rc_testdata.get_svn_repo_archive()
800 787 with tarfile.open(svn_repo_path) as tar:
801 788 tar.extractall(jn(test_path, SVN_REPO))
802 789
803 790
804 791 #==============================================================================
805 792 # PASTER COMMANDS
806 793 #==============================================================================
807 794 class BasePasterCommand(Command):
808 795 """
809 796 Abstract Base Class for paster commands.
810 797
811 798 The celery commands are somewhat aggressive about loading
812 799 celery.conf, and since our module sets the `CELERY_LOADER`
813 800 environment variable to our loader, we have to bootstrap a bit and
814 801 make sure we've had a chance to load the pylons config off of the
815 802 command line, otherwise everything fails.
816 803 """
817 804 min_args = 1
818 805 min_args_error = "Please provide a paster config file as an argument."
819 806 takes_config_file = 1
820 807 requires_config_file = True
821 808
822 809 def notify_msg(self, msg, log=False):
823 810 """Make a notification to user, additionally if logger is passed
824 811 it logs this action using given logger
825 812
826 813 :param msg: message that will be printed to user
827 814 :param log: logging instance, to use to additionally log this message
828 815
829 816 """
830 817 if log and isinstance(log, logging):
831 818 log(msg)
832 819
833 820 def run(self, args):
834 821 """
835 822 Overrides Command.run
836 823
837 824 Checks for a config file argument and loads it.
838 825 """
839 826 if len(args) < self.min_args:
840 827 raise BadCommand(
841 828 self.min_args_error % {'min_args': self.min_args,
842 829 'actual_args': len(args)})
843 830
844 831 # Decrement because we're going to lob off the first argument.
845 832 # @@ This is hacky
846 833 self.min_args -= 1
847 834 self.bootstrap_config(args[0])
848 835 self.update_parser()
849 836 return super(BasePasterCommand, self).run(args[1:])
850 837
851 838 def update_parser(self):
852 839 """
853 840 Abstract method. Allows for the class' parser to be updated
854 841 before the superclass' `run` method is called. Necessary to
855 842 allow options/arguments to be passed through to the underlying
856 843 celery command.
857 844 """
858 845 raise NotImplementedError("Abstract Method.")
859 846
860 847 def bootstrap_config(self, conf):
861 848 """
862 849 Loads the pylons configuration.
863 850 """
864 851 from pylons import config as pylonsconfig
865 852
866 853 self.path_to_ini_file = os.path.realpath(conf)
867 854 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
868 855 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
869 856
870 857 def _init_session(self):
871 858 """
872 859 Inits SqlAlchemy Session
873 860 """
874 861 logging.config.fileConfig(self.path_to_ini_file)
875 862 from pylons import config
876 863 from rhodecode.config.utils import initialize_database
877 864
878 865 # get to remove repos !!
879 866 add_cache(config)
880 867 initialize_database(config)
881 868
882 869
883 870 @decorator.decorator
884 871 def jsonify(func, *args, **kwargs):
885 872 """Action decorator that formats output for JSON
886 873
887 874 Given a function that will return content, this decorator will turn
888 875 the result into JSON, with a content-type of 'application/json' and
889 876 output it.
890 877
891 878 """
892 879 from pylons.decorators.util import get_pylons
893 880 from rhodecode.lib.ext_json import json
894 881 pylons = get_pylons(args)
895 882 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
896 883 data = func(*args, **kwargs)
897 884 if isinstance(data, (list, tuple)):
898 885 msg = "JSON responses with Array envelopes are susceptible to " \
899 886 "cross-site data leak attacks, see " \
900 887 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
901 888 warnings.warn(msg, Warning, 2)
902 889 log.warning(msg)
903 890 log.debug("Returning JSON wrapped action output")
904 891 return json.dumps(data, encoding='utf-8')
905 892
906 893
907 894 class PartialRenderer(object):
908 895 """
909 896 Partial renderer used to render chunks of html used in datagrids
910 897 use like::
911 898
912 899 _render = PartialRenderer('data_table/_dt_elements.html')
913 900 _render('quick_menu', args, kwargs)
914 901 PartialRenderer.h,
915 902 c,
916 903 _,
917 904 ungettext
918 905 are the template stuff initialized inside and can be re-used later
919 906
920 907 :param tmpl_name: template path relate to /templates/ dir
921 908 """
922 909
923 910 def __init__(self, tmpl_name):
924 911 import rhodecode
925 912 from pylons import request, tmpl_context as c
926 913 from pylons.i18n.translation import _, ungettext
927 914 from rhodecode.lib import helpers as h
928 915
929 916 self.tmpl_name = tmpl_name
930 917 self.rhodecode = rhodecode
931 918 self.c = c
932 919 self._ = _
933 920 self.ungettext = ungettext
934 921 self.h = h
935 922 self.request = request
936 923
937 924 def _mako_lookup(self):
938 925 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
939 926 return _tmpl_lookup.get_template(self.tmpl_name)
940 927
941 928 def _update_kwargs_for_render(self, kwargs):
942 929 """
943 930 Inject params required for Mako rendering
944 931 """
945 932 _kwargs = {
946 933 '_': self._,
947 934 'h': self.h,
948 935 'c': self.c,
949 936 'request': self.request,
950 937 'ungettext': self.ungettext,
951 938 }
952 939 _kwargs.update(kwargs)
953 940 return _kwargs
954 941
955 942 def _render_with_exc(self, render_func, args, kwargs):
956 943 try:
957 944 return render_func.render(*args, **kwargs)
958 945 except:
959 946 log.error(exceptions.text_error_template().render())
960 947 raise
961 948
962 949 def _get_template(self, template_obj, def_name):
963 950 if def_name:
964 951 tmpl = template_obj.get_def(def_name)
965 952 else:
966 953 tmpl = template_obj
967 954 return tmpl
968 955
969 956 def render(self, def_name, *args, **kwargs):
970 957 lookup_obj = self._mako_lookup()
971 958 tmpl = self._get_template(lookup_obj, def_name=def_name)
972 959 kwargs = self._update_kwargs_for_render(kwargs)
973 960 return self._render_with_exc(tmpl, args, kwargs)
974 961
975 962 def __call__(self, tmpl, *args, **kwargs):
976 963 return self.render(tmpl, *args, **kwargs)
977 964
978 965
979 966 def password_changed(auth_user, session):
980 967 # Never report password change in case of default user or anonymous user.
981 968 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
982 969 return False
983 970
984 971 password_hash = md5(auth_user.password) if auth_user.password else None
985 972 rhodecode_user = session.get('rhodecode_user', {})
986 973 session_password_hash = rhodecode_user.get('password', '')
987 974 return password_hash != session_password_hash
988 975
989 976
990 977 def read_opensource_licenses():
991 978 global _license_cache
992 979
993 980 if not _license_cache:
994 981 licenses = pkg_resources.resource_string(
995 982 'rhodecode', 'config/licenses.json')
996 983 _license_cache = json.loads(licenses)
997 984
998 985 return _license_cache
999 986
1000 987
1001 988 def get_registry(request):
1002 989 """
1003 990 Utility to get the pyramid registry from a request. During migration to
1004 991 pyramid we sometimes want to use the pyramid registry from pylons context.
1005 992 Therefore this utility returns `request.registry` for pyramid requests and
1006 993 uses `get_current_registry()` for pylons requests.
1007 994 """
1008 995 try:
1009 996 return request.registry
1010 997 except AttributeError:
1011 998 return get_current_registry()
General Comments 0
You need to be logged in to leave comments. Login now