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