##// END OF EJS Templates
vcs: handle excessive slashes in from of the repo name path, fixes #5522
marcink -
r3328:44e97172 default
parent child Browse files
Show More
@@ -1,157 +1,160 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleHG middleware for handling mercurial protocol request
23 23 (push/clone etc.). It's implemented with basic auth function
24 24 """
25 25
26 26 import logging
27 27 import urlparse
28 28 import urllib
29 29
30 30 from rhodecode.lib import utils
31 31 from rhodecode.lib.ext_json import json
32 32 from rhodecode.lib.middleware import simplevcs
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class SimpleHg(simplevcs.SimpleVCS):
38 38
39 39 SCM = 'hg'
40 40
41 41 def _get_repository_name(self, environ):
42 42 """
43 43 Gets repository name out of PATH_INFO header
44 44
45 45 :param environ: environ where PATH_INFO is stored
46 46 """
47 return environ['PATH_INFO'].strip('/')
47 repo_name = environ['PATH_INFO']
48 if repo_name and repo_name.startswith('/'):
49 # remove only the first leading /
50 repo_name = repo_name[1:]
51 return repo_name.rstrip('/')
48 52
49 53 _ACTION_MAPPING = {
50 54 'changegroup': 'pull',
51 55 'changegroupsubset': 'pull',
52 56 'getbundle': 'pull',
53 57 'stream_out': 'pull',
54 58 'listkeys': 'pull',
55 59 'between': 'pull',
56 60 'branchmap': 'pull',
57 61 'branches': 'pull',
58 62 'clonebundles': 'pull',
59 63 'capabilities': 'pull',
60 64 'debugwireargs': 'pull',
61 65 'heads': 'pull',
62 66 'lookup': 'pull',
63 67 'hello': 'pull',
64 68 'known': 'pull',
65 69
66 70 # largefiles
67 71 'putlfile': 'push',
68 72 'getlfile': 'pull',
69 73 'statlfile': 'pull',
70 74 'lheads': 'pull',
71 75
72 76 # evolve
73 77 'evoext_obshashrange_v1': 'pull',
74 78 'evoext_obshash': 'pull',
75 79 'evoext_obshash1': 'pull',
76 80
77 81 'unbundle': 'push',
78 82 'pushkey': 'push',
79 83 }
80 84
81 85 @classmethod
82 86 def _get_xarg_headers(cls, environ):
83 87 i = 1
84 88 chunks = [] # gather chunks stored in multiple 'hgarg_N'
85 89 while True:
86 90 head = environ.get('HTTP_X_HGARG_{}'.format(i))
87 91 if not head:
88 92 break
89 93 i += 1
90 94 chunks.append(urllib.unquote_plus(head))
91 95 full_arg = ''.join(chunks)
92 96 pref = 'cmds='
93 97 if full_arg.startswith(pref):
94 98 # strip the cmds= header defining our batch commands
95 99 full_arg = full_arg[len(pref):]
96 100 cmds = full_arg.split(';')
97 101 return cmds
98 102
99 103 @classmethod
100 104 def _get_batch_cmd(cls, environ):
101 105 """
102 106 Handle batch command send commands. Those are ';' separated commands
103 107 sent by batch command that server needs to execute. We need to extract
104 108 those, and map them to our ACTION_MAPPING to get all push/pull commands
105 109 specified in the batch
106 110 """
107 111 default = 'push'
108 112 batch_cmds = []
109 113 try:
110 114 cmds = cls._get_xarg_headers(environ)
111 115 for pair in cmds:
112 116 parts = pair.split(' ', 1)
113 117 if len(parts) != 2:
114 118 continue
115 119 # entry should be in a format `key ARGS`
116 120 cmd, args = parts
117 121 action = cls._ACTION_MAPPING.get(cmd, default)
118 122 batch_cmds.append(action)
119 123 except Exception:
120 124 log.exception('Failed to extract batch commands operations')
121 125
122 126 # in case we failed, (e.g malformed data) assume it's PUSH sub-command
123 127 # for safety
124 128 return batch_cmds or [default]
125 129
126 130 def _get_action(self, environ):
127 131 """
128 132 Maps mercurial request commands into a pull or push command.
129 133 In case of unknown/unexpected data, it returns 'push' to be safe.
130 134
131 135 :param environ:
132 136 """
133 137 default = 'push'
134 138 query = urlparse.parse_qs(environ['QUERY_STRING'],
135 139 keep_blank_values=True)
136 140
137 141 if 'cmd' in query:
138 142 cmd = query['cmd'][0]
139 143 if cmd == 'batch':
140 144 cmds = self._get_batch_cmd(environ)
141 145 if 'push' in cmds:
142 146 return 'push'
143 147 else:
144 148 return 'pull'
145 149 return self._ACTION_MAPPING.get(cmd, default)
146 150
147 151 return default
148 152
149 153 def _create_wsgi_app(self, repo_path, repo_name, config):
150 return self.scm_app.create_hg_wsgi_app(
151 repo_path, repo_name, config)
154 return self.scm_app.create_hg_wsgi_app(repo_path, repo_name, config)
152 155
153 156 def _create_config(self, extras, repo_name):
154 157 config = utils.make_db_config(repo=repo_name)
155 158 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
156 159
157 160 return config.serialize()
@@ -1,673 +1,669 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31 from StringIO import StringIO
32 32 from lxml import etree
33 33
34 34 import time
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from pyramid.httpexceptions import (
38 38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 39 from zope.cachedescriptors.property import Lazy as LazyProperty
40 40
41 41 import rhodecode
42 42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 43 from rhodecode.lib import rc_cache
44 44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 45 from rhodecode.lib.base import (
46 46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 49 from rhodecode.lib.middleware import appenlight
50 50 from rhodecode.lib.middleware.utils import scm_app_http
51 51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 54 from rhodecode.lib.vcs.backends import base
55 55
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def extract_svn_txn_id(acl_repo_name, data):
66 66 """
67 67 Helper method for extraction of svn txn_id from submitted XML data during
68 68 POST operations
69 69 """
70 70 try:
71 71 root = etree.fromstring(data)
72 72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 73 for el in root:
74 74 if el.tag == '{DAV:}source':
75 75 for sub_el in el:
76 76 if sub_el.tag == '{DAV:}href':
77 77 match = pat.search(sub_el.text)
78 78 if match:
79 79 svn_tx_id = match.groupdict()['txn_id']
80 80 txn_id = rc_cache.utils.compute_key_from_params(
81 81 acl_repo_name, svn_tx_id)
82 82 return txn_id
83 83 except Exception:
84 84 log.exception('Failed to extract txn_id')
85 85
86 86
87 87 def initialize_generator(factory):
88 88 """
89 89 Initializes the returned generator by draining its first element.
90 90
91 91 This can be used to give a generator an initializer, which is the code
92 92 up to the first yield statement. This decorator enforces that the first
93 93 produced element has the value ``"__init__"`` to make its special
94 94 purpose very explicit in the using code.
95 95 """
96 96
97 97 @wraps(factory)
98 98 def wrapper(*args, **kwargs):
99 99 gen = factory(*args, **kwargs)
100 100 try:
101 101 init = gen.next()
102 102 except StopIteration:
103 103 raise ValueError('Generator must yield at least one element.')
104 104 if init != "__init__":
105 105 raise ValueError('First yielded element must be "__init__".')
106 106 return gen
107 107 return wrapper
108 108
109 109
110 110 class SimpleVCS(object):
111 111 """Common functionality for SCM HTTP handlers."""
112 112
113 113 SCM = 'unknown'
114 114
115 115 acl_repo_name = None
116 116 url_repo_name = None
117 117 vcs_repo_name = None
118 118 rc_extras = {}
119 119
120 120 # We have to handle requests to shadow repositories different than requests
121 121 # to normal repositories. Therefore we have to distinguish them. To do this
122 122 # we use this regex which will match only on URLs pointing to shadow
123 123 # repositories.
124 124 shadow_repo_re = re.compile(
125 125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 126 '(?P<target>{slug_pat})/' # target repo
127 127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 128 'repository$' # shadow repo
129 129 .format(slug_pat=SLUG_RE.pattern))
130 130
131 131 def __init__(self, config, registry):
132 132 self.registry = registry
133 133 self.config = config
134 134 # re-populated by specialized middleware
135 135 self.repo_vcs_config = base.Config()
136 136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137 137
138 138 registry.rhodecode_settings = self.rhodecode_settings
139 139 # authenticate this VCS request using authfunc
140 140 auth_ret_code_detection = \
141 141 str2bool(self.config.get('auth_ret_code_detection', False))
142 142 self.authenticate = BasicAuth(
143 143 '', authenticate, registry, config.get('auth_ret_code'),
144 144 auth_ret_code_detection)
145 145 self.ip_addr = '0.0.0.0'
146 146
147 147 @LazyProperty
148 148 def global_vcs_config(self):
149 149 try:
150 150 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 151 except Exception:
152 152 return base.Config()
153 153
154 154 @property
155 155 def base_path(self):
156 settings_path = self.repo_vcs_config.get(
157 *VcsSettingsModel.PATH_SETTING)
156 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
158 157
159 158 if not settings_path:
160 settings_path = self.global_vcs_config.get(
161 *VcsSettingsModel.PATH_SETTING)
159 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
162 160
163 161 if not settings_path:
164 162 # try, maybe we passed in explicitly as config option
165 163 settings_path = self.config.get('base_path')
166 164
167 165 if not settings_path:
168 166 raise ValueError('FATAL: base_path is empty')
169 167 return settings_path
170 168
171 169 def set_repo_names(self, environ):
172 170 """
173 171 This will populate the attributes acl_repo_name, url_repo_name,
174 172 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 173 shadow) repositories all names are equal. In case of requests to a
176 174 shadow repository the acl-name points to the target repo of the pull
177 175 request and the vcs-name points to the shadow repo file system path.
178 176 The url-name is always the URL used by the vcs client program.
179 177
180 178 Example in case of a shadow repo:
181 179 acl_repo_name = RepoGroup/MyRepo
182 180 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 181 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 182 """
185 183 # First we set the repo name from URL for all attributes. This is the
186 184 # default if handling normal (non shadow) repo requests.
187 185 self.url_repo_name = self._get_repository_name(environ)
188 186 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 187 self.is_shadow_repo = False
190 188
191 189 # Check if this is a request to a shadow repository.
192 190 match = self.shadow_repo_re.match(self.url_repo_name)
193 191 if match:
194 192 match_dict = match.groupdict()
195 193
196 194 # Build acl repo name from regex match.
197 195 acl_repo_name = safe_unicode('{groups}{target}'.format(
198 196 groups=match_dict['groups'] or '',
199 197 target=match_dict['target']))
200 198
201 199 # Retrieve pull request instance by ID from regex match.
202 200 pull_request = PullRequest.get(match_dict['pr_id'])
203 201
204 202 # Only proceed if we got a pull request and if acl repo name from
205 203 # URL equals the target repo name of the pull request.
206 204 if pull_request and \
207 205 (acl_repo_name == pull_request.target_repo.repo_name):
208 206 repo_id = pull_request.target_repo.repo_id
209 207 # Get file system path to shadow repository.
210 208 workspace_id = PullRequestModel()._workspace_id(pull_request)
211 209 target_vcs = pull_request.target_repo.scm_instance()
212 210 vcs_repo_name = target_vcs._get_shadow_repository_path(
213 211 repo_id, workspace_id)
214 212
215 213 # Store names for later usage.
216 214 self.vcs_repo_name = vcs_repo_name
217 215 self.acl_repo_name = acl_repo_name
218 216 self.is_shadow_repo = True
219 217
220 218 log.debug('Setting all VCS repository names: %s', {
221 219 'acl_repo_name': self.acl_repo_name,
222 220 'url_repo_name': self.url_repo_name,
223 221 'vcs_repo_name': self.vcs_repo_name,
224 222 })
225 223
226 224 @property
227 225 def scm_app(self):
228 226 custom_implementation = self.config['vcs.scm_app_implementation']
229 227 if custom_implementation == 'http':
230 228 log.info('Using HTTP implementation of scm app.')
231 229 scm_app_impl = scm_app_http
232 230 else:
233 231 log.info('Using custom implementation of scm_app: "{}"'.format(
234 232 custom_implementation))
235 233 scm_app_impl = importlib.import_module(custom_implementation)
236 234 return scm_app_impl
237 235
238 236 def _get_by_id(self, repo_name):
239 237 """
240 238 Gets a special pattern _<ID> from clone url and tries to replace it
241 239 with a repository_name for support of _<ID> non changeable urls
242 240 """
243 241
244 242 data = repo_name.split('/')
245 243 if len(data) >= 2:
246 244 from rhodecode.model.repo import RepoModel
247 245 by_id_match = RepoModel().get_repo_by_id(repo_name)
248 246 if by_id_match:
249 247 data[1] = by_id_match.repo_name
250 248
251 249 return safe_str('/'.join(data))
252 250
253 251 def _invalidate_cache(self, repo_name):
254 252 """
255 253 Set's cache for this repository for invalidation on next access
256 254
257 255 :param repo_name: full repo name, also a cache key
258 256 """
259 257 ScmModel().mark_for_invalidation(repo_name)
260 258
261 259 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 260 db_repo = Repository.get_by_repo_name(repo_name)
263 261 if not db_repo:
264 262 log.debug('Repository `%s` not found inside the database.',
265 263 repo_name)
266 264 return False
267 265
268 266 if db_repo.repo_type != scm_type:
269 267 log.warning(
270 268 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 269 repo_name, db_repo.repo_type, scm_type)
272 270 return False
273 271
274 272 config = db_repo._config
275 273 config.set('extensions', 'largefiles', '')
276 274 return is_valid_repo(
277 275 repo_name, base_path,
278 276 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279 277
280 278 def valid_and_active_user(self, user):
281 279 """
282 280 Checks if that user is not empty, and if it's actually object it checks
283 281 if he's active.
284 282
285 283 :param user: user object or None
286 284 :return: boolean
287 285 """
288 286 if user is None:
289 287 return False
290 288
291 289 elif user.active:
292 290 return True
293 291
294 292 return False
295 293
296 294 @property
297 295 def is_shadow_repo_dir(self):
298 296 return os.path.isdir(self.vcs_repo_name)
299 297
300 298 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
301 299 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 300 """
303 301 Checks permissions using action (push/pull) user and repository
304 302 name. If plugin_cache and ttl is set it will use the plugin which
305 303 authenticated the user to store the cached permissions result for N
306 304 amount of seconds as in cache_ttl
307 305
308 306 :param action: push or pull action
309 307 :param user: user instance
310 308 :param repo_name: repository name
311 309 """
312 310
313 311 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 312 plugin_id, plugin_cache_active, cache_ttl)
315 313
316 314 user_id = user.user_id
317 315 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 316 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319 317
320 318 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 319 expiration_time=cache_ttl,
322 320 condition=plugin_cache_active)
323 321 def compute_perm_vcs(
324 322 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325 323
326 324 log.debug('auth: calculating permission access now...')
327 325 # check IP
328 326 inherit = user.inherit_default_permissions
329 327 ip_allowed = AuthUser.check_ip_allowed(
330 328 user_id, ip_addr, inherit_from_default=inherit)
331 329 if ip_allowed:
332 330 log.info('Access for IP:%s allowed', ip_addr)
333 331 else:
334 332 return False
335 333
336 334 if action == 'push':
337 335 perms = ('repository.write', 'repository.admin')
338 336 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
339 337 return False
340 338
341 339 else:
342 340 # any other action need at least read permission
343 341 perms = (
344 342 'repository.read', 'repository.write', 'repository.admin')
345 343 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
346 344 return False
347 345
348 346 return True
349 347
350 348 start = time.time()
351 349 log.debug('Running plugin `%s` permissions check', plugin_id)
352 350
353 351 # for environ based auth, password can be empty, but then the validation is
354 352 # on the server that fills in the env data needed for authentication
355 353 perm_result = compute_perm_vcs(
356 354 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357 355
358 356 auth_time = time.time() - start
359 357 log.debug('Permissions for plugin `%s` completed in %.3fs, '
360 358 'expiration time of fetched cache %.1fs.',
361 359 plugin_id, auth_time, cache_ttl)
362 360
363 361 return perm_result
364 362
365 363 def _check_ssl(self, environ, start_response):
366 364 """
367 365 Checks the SSL check flag and returns False if SSL is not present
368 366 and required True otherwise
369 367 """
370 368 org_proto = environ['wsgi._org_proto']
371 369 # check if we have SSL required ! if not it's a bad request !
372 370 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
373 371 if require_ssl and org_proto == 'http':
374 372 log.debug(
375 373 'Bad request: detected protocol is `%s` and '
376 374 'SSL/HTTPS is required.', org_proto)
377 375 return False
378 376 return True
379 377
380 378 def _get_default_cache_ttl(self):
381 379 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
382 380 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
383 381 plugin_settings = plugin.get_settings()
384 382 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
385 383 plugin_settings) or (False, 0)
386 384 return plugin_cache_active, cache_ttl
387 385
388 386 def __call__(self, environ, start_response):
389 387 try:
390 388 return self._handle_request(environ, start_response)
391 389 except Exception:
392 390 log.exception("Exception while handling request")
393 391 appenlight.track_exception(environ)
394 392 return HTTPInternalServerError()(environ, start_response)
395 393 finally:
396 394 meta.Session.remove()
397 395
398 396 def _handle_request(self, environ, start_response):
399
400 397 if not self._check_ssl(environ, start_response):
401 398 reason = ('SSL required, while RhodeCode was unable '
402 399 'to detect this as SSL request')
403 400 log.debug('User not allowed to proceed, %s', reason)
404 401 return HTTPNotAcceptable(reason)(environ, start_response)
405 402
406 403 if not self.url_repo_name:
407 404 log.warning('Repository name is empty: %s', self.url_repo_name)
408 405 # failed to get repo name, we fail now
409 406 return HTTPNotFound()(environ, start_response)
410 407 log.debug('Extracted repo name is %s', self.url_repo_name)
411 408
412 409 ip_addr = get_ip_addr(environ)
413 410 user_agent = get_user_agent(environ)
414 411 username = None
415 412
416 413 # skip passing error to error controller
417 414 environ['pylons.status_code_redirect'] = True
418 415
419 416 # ======================================================================
420 417 # GET ACTION PULL or PUSH
421 418 # ======================================================================
422 419 action = self._get_action(environ)
423 420
424 421 # ======================================================================
425 422 # Check if this is a request to a shadow repository of a pull request.
426 423 # In this case only pull action is allowed.
427 424 # ======================================================================
428 425 if self.is_shadow_repo and action != 'pull':
429 426 reason = 'Only pull action is allowed for shadow repositories.'
430 427 log.debug('User not allowed to proceed, %s', reason)
431 428 return HTTPNotAcceptable(reason)(environ, start_response)
432 429
433 430 # Check if the shadow repo actually exists, in case someone refers
434 431 # to it, and it has been deleted because of successful merge.
435 432 if self.is_shadow_repo and not self.is_shadow_repo_dir:
436 433 log.debug(
437 434 'Shadow repo detected, and shadow repo dir `%s` is missing',
438 435 self.is_shadow_repo_dir)
439 436 return HTTPNotFound()(environ, start_response)
440 437
441 438 # ======================================================================
442 439 # CHECK ANONYMOUS PERMISSION
443 440 # ======================================================================
444 441 detect_force_push = False
445 442 check_branch_perms = False
446 443 if action in ['pull', 'push']:
447 444 user_obj = anonymous_user = User.get_default_user()
448 445 auth_user = user_obj.AuthUser()
449 446 username = anonymous_user.username
450 447 if anonymous_user.active:
451 448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
452 449 # ONLY check permissions if the user is activated
453 450 anonymous_perm = self._check_permission(
454 451 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
455 452 plugin_id='anonymous_access',
456 453 plugin_cache_active=plugin_cache_active,
457 454 cache_ttl=cache_ttl,
458 455 )
459 456 else:
460 457 anonymous_perm = False
461 458
462 459 if not anonymous_user.active or not anonymous_perm:
463 460 if not anonymous_user.active:
464 461 log.debug('Anonymous access is disabled, running '
465 462 'authentication')
466 463
467 464 if not anonymous_perm:
468 465 log.debug('Not enough credentials to access this '
469 466 'repository as anonymous user')
470 467
471 468 username = None
472 469 # ==============================================================
473 470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
474 471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
475 472 # ==============================================================
476 473
477 474 # try to auth based on environ, container auth methods
478 475 log.debug('Running PRE-AUTH for container based authentication')
479 476 pre_auth = authenticate(
480 477 '', '', environ, VCS_TYPE, registry=self.registry,
481 478 acl_repo_name=self.acl_repo_name)
482 479 if pre_auth and pre_auth.get('username'):
483 480 username = pre_auth['username']
484 481 log.debug('PRE-AUTH got %s as username', username)
485 482 if pre_auth:
486 483 log.debug('PRE-AUTH successful from %s',
487 484 pre_auth.get('auth_data', {}).get('_plugin'))
488 485
489 486 # If not authenticated by the container, running basic auth
490 487 # before inject the calling repo_name for special scope checks
491 488 self.authenticate.acl_repo_name = self.acl_repo_name
492 489
493 490 plugin_cache_active, cache_ttl = False, 0
494 491 plugin = None
495 492 if not username:
496 493 self.authenticate.realm = self.authenticate.get_rc_realm()
497 494
498 495 try:
499 496 auth_result = self.authenticate(environ)
500 497 except (UserCreationError, NotAllowedToCreateUserError) as e:
501 498 log.error(e)
502 499 reason = safe_str(e)
503 500 return HTTPNotAcceptable(reason)(environ, start_response)
504 501
505 502 if isinstance(auth_result, dict):
506 503 AUTH_TYPE.update(environ, 'basic')
507 504 REMOTE_USER.update(environ, auth_result['username'])
508 505 username = auth_result['username']
509 506 plugin = auth_result.get('auth_data', {}).get('_plugin')
510 507 log.info(
511 508 'MAIN-AUTH successful for user `%s` from %s plugin',
512 509 username, plugin)
513 510
514 511 plugin_cache_active, cache_ttl = auth_result.get(
515 512 'auth_data', {}).get('_ttl_cache') or (False, 0)
516 513 else:
517 return auth_result.wsgi_application(
518 environ, start_response)
514 return auth_result.wsgi_application(environ, start_response)
519 515
520 516 # ==============================================================
521 517 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
522 518 # ==============================================================
523 519 user = User.get_by_username(username)
524 520 if not self.valid_and_active_user(user):
525 521 return HTTPForbidden()(environ, start_response)
526 522 username = user.username
527 523 user_id = user.user_id
528 524
529 525 # check user attributes for password change flag
530 526 user_obj = user
531 527 auth_user = user_obj.AuthUser()
532 528 if user_obj and user_obj.username != User.DEFAULT_USER and \
533 529 user_obj.user_data.get('force_password_change'):
534 530 reason = 'password change required'
535 531 log.debug('User not allowed to authenticate, %s', reason)
536 532 return HTTPNotAcceptable(reason)(environ, start_response)
537 533
538 534 # check permissions for this repository
539 535 perm = self._check_permission(
540 536 action, user, auth_user, self.acl_repo_name, ip_addr,
541 537 plugin, plugin_cache_active, cache_ttl)
542 538 if not perm:
543 539 return HTTPForbidden()(environ, start_response)
544 540 environ['rc_auth_user_id'] = user_id
545 541
546 542 if action == 'push':
547 543 perms = auth_user.get_branch_permissions(self.acl_repo_name)
548 544 if perms:
549 545 check_branch_perms = True
550 546 detect_force_push = True
551 547
552 548 # extras are injected into UI object and later available
553 549 # in hooks executed by RhodeCode
554 550 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
555 551
556 552 extras = vcs_operation_context(
557 553 environ, repo_name=self.acl_repo_name, username=username,
558 554 action=action, scm=self.SCM, check_locking=check_locking,
559 555 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
560 556 detect_force_push=detect_force_push
561 557 )
562 558
563 559 # ======================================================================
564 560 # REQUEST HANDLING
565 561 # ======================================================================
566 562 repo_path = os.path.join(
567 563 safe_str(self.base_path), safe_str(self.vcs_repo_name))
568 564 log.debug('Repository path is %s', repo_path)
569 565
570 566 fix_PATH()
571 567
572 568 log.info(
573 569 '%s action on %s repo "%s" by "%s" from %s %s',
574 570 action, self.SCM, safe_str(self.url_repo_name),
575 571 safe_str(username), ip_addr, user_agent)
576 572
577 573 return self._generate_vcs_response(
578 574 environ, start_response, repo_path, extras, action)
579 575
580 576 @initialize_generator
581 577 def _generate_vcs_response(
582 578 self, environ, start_response, repo_path, extras, action):
583 579 """
584 580 Returns a generator for the response content.
585 581
586 582 This method is implemented as a generator, so that it can trigger
587 583 the cache validation after all content sent back to the client. It
588 584 also handles the locking exceptions which will be triggered when
589 585 the first chunk is produced by the underlying WSGI application.
590 586 """
591 587 txn_id = ''
592 588 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
593 589 # case for SVN, we want to re-use the callback daemon port
594 590 # so we use the txn_id, for this we peek the body, and still save
595 591 # it as wsgi.input
596 592 data = environ['wsgi.input'].read()
597 593 environ['wsgi.input'] = StringIO(data)
598 594 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
599 595
600 596 callback_daemon, extras = self._prepare_callback_daemon(
601 597 extras, environ, action, txn_id=txn_id)
602 598 log.debug('HOOKS extras is %s', extras)
603 599
604 600 config = self._create_config(extras, self.acl_repo_name)
605 601 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
606 602 with callback_daemon:
607 603 app.rc_extras = extras
608 604
609 605 try:
610 606 response = app(environ, start_response)
611 607 finally:
612 608 # This statement works together with the decorator
613 609 # "initialize_generator" above. The decorator ensures that
614 610 # we hit the first yield statement before the generator is
615 611 # returned back to the WSGI server. This is needed to
616 612 # ensure that the call to "app" above triggers the
617 613 # needed callback to "start_response" before the
618 614 # generator is actually used.
619 615 yield "__init__"
620 616
621 617 # iter content
622 618 for chunk in response:
623 619 yield chunk
624 620
625 621 try:
626 622 # invalidate cache on push
627 623 if action == 'push':
628 624 self._invalidate_cache(self.url_repo_name)
629 625 finally:
630 626 meta.Session.remove()
631 627
632 628 def _get_repository_name(self, environ):
633 629 """Get repository name out of the environmnent
634 630
635 631 :param environ: WSGI environment
636 632 """
637 633 raise NotImplementedError()
638 634
639 635 def _get_action(self, environ):
640 636 """Map request commands into a pull or push command.
641 637
642 638 :param environ: WSGI environment
643 639 """
644 640 raise NotImplementedError()
645 641
646 642 def _create_wsgi_app(self, repo_path, repo_name, config):
647 643 """Return the WSGI app that will finally handle the request."""
648 644 raise NotImplementedError()
649 645
650 646 def _create_config(self, extras, repo_name):
651 647 """Create a safe config representation."""
652 648 raise NotImplementedError()
653 649
654 650 def _should_use_callback_daemon(self, extras, environ, action):
655 651 return True
656 652
657 653 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
658 654 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
659 655 if not self._should_use_callback_daemon(extras, environ, action):
660 656 # disable callback daemon for actions that don't require it
661 657 direct_calls = True
662 658
663 659 return prepare_callback_daemon(
664 660 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
665 661 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
666 662
667 663
668 664 def _should_check_locking(query_string):
669 665 # this is kind of hacky, but due to how mercurial handles client-server
670 666 # server see all operation on commit; bookmarks, phases and
671 667 # obsolescence marker in different transaction, we don't want to check
672 668 # locking on those
673 669 return query_string not in ['cmd=listkeys']
@@ -1,459 +1,469 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Test suite for making push/pull operations, on specially modified INI files
23 23
24 24 .. important::
25 25
26 26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 27 to redirect things to stderr instead of stdout.
28 28 """
29 29
30 30
31 31 import time
32 32
33 33 import pytest
34 34
35 35 from rhodecode.lib import rc_cache
36 36 from rhodecode.model.auth_token import AuthTokenModel
37 37 from rhodecode.model.db import Repository, UserIpMap, CacheKey
38 38 from rhodecode.model.meta import Session
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.user import UserModel
41 41 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
42 42
43 43 from rhodecode.tests.vcs_operations import (
44 44 Command, _check_proper_clone, _check_proper_git_push,
45 45 _add_files_and_push, HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP)
46 46
47 47
48 48 @pytest.mark.usefixtures("disable_locking", "disable_anonymous_user")
49 49 class TestVCSOperations(object):
50 50
51 51 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
52 52 clone_url = rc_web_server.repo_clone_url(HG_REPO)
53 53 stdout, stderr = Command('/tmp').execute(
54 54 'hg clone', clone_url, tmpdir.strpath)
55 55 _check_proper_clone(stdout, stderr, 'hg')
56 56
57 57 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
58 58 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
59 59 cmd = Command('/tmp')
60 60 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
61 61 _check_proper_clone(stdout, stderr, 'git')
62 62 cmd.assert_returncode_success()
63 63
64 64 def test_clone_git_repo_by_admin_with_git_suffix(self, rc_web_server, tmpdir):
65 65 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
66 66 cmd = Command('/tmp')
67 67 stdout, stderr = cmd.execute('git clone', clone_url+".git", tmpdir.strpath)
68 68 _check_proper_clone(stdout, stderr, 'git')
69 69 cmd.assert_returncode_success()
70 70
71 71 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
72 72 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
73 73 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
74 74 stdout, stderr = Command('/tmp').execute(
75 75 'hg clone', clone_url, tmpdir.strpath)
76 76 _check_proper_clone(stdout, stderr, 'hg')
77 77
78 78 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
79 79 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
80 80 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
81 81 cmd = Command('/tmp')
82 82 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
83 83 _check_proper_clone(stdout, stderr, 'git')
84 84 cmd.assert_returncode_success()
85 85
86 86 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
87 87 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
88 88 stdout, stderr = Command('/tmp').execute(
89 89 'hg clone', clone_url, tmpdir.strpath)
90 90 _check_proper_clone(stdout, stderr, 'hg')
91 91
92 92 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
93 93 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
94 94 cmd = Command('/tmp')
95 95 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
96 96 _check_proper_clone(stdout, stderr, 'git')
97 97 cmd.assert_returncode_success()
98 98
99 99 def test_clone_git_repo_shallow_by_admin(self, rc_web_server, tmpdir):
100 100 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
101 101 cmd = Command('/tmp')
102 102 stdout, stderr = cmd.execute(
103 103 'git clone --depth=1', clone_url, tmpdir.strpath)
104 104
105 105 assert '' == stdout
106 106 assert 'Cloning into' in stderr
107 107 cmd.assert_returncode_success()
108 108
109 109 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
110 110 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
111 111 stdout, stderr = Command('/tmp').execute(
112 112 'hg clone', clone_url, tmpdir.strpath)
113 113 assert 'abort: authorization failed' in stderr
114 114
115 115 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
116 116 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
117 117 stdout, stderr = Command('/tmp').execute(
118 118 'git clone', clone_url, tmpdir.strpath)
119 119 assert 'fatal: Authentication failed' in stderr
120 120
121 121 def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir):
122 122 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
123 123 stdout, stderr = Command('/tmp').execute(
124 124 'hg clone', clone_url, tmpdir.strpath)
125 125 assert 'HTTP Error 404: Not Found' in stderr
126 126
127 127 def test_clone_hg_repo_as_git(self, rc_web_server, tmpdir):
128 128 clone_url = rc_web_server.repo_clone_url(HG_REPO)
129 129 stdout, stderr = Command('/tmp').execute(
130 130 'git clone', clone_url, tmpdir.strpath)
131 131 assert 'not found' in stderr
132 132
133 133 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
134 134 clone_url = rc_web_server.repo_clone_url('trololo')
135 135 stdout, stderr = Command('/tmp').execute(
136 136 'hg clone', clone_url, tmpdir.strpath)
137 137 assert 'HTTP Error 404: Not Found' in stderr
138 138
139 139 def test_clone_non_existing_path_git(self, rc_web_server, tmpdir):
140 140 clone_url = rc_web_server.repo_clone_url('trololo')
141 141 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
142 142 assert 'not found' in stderr
143 143
144 def test_clone_hg_with_slashes(self, rc_web_server, tmpdir):
145 clone_url = rc_web_server.repo_clone_url('//' + HG_REPO)
146 stdout, stderr = Command('/tmp').execute('hg clone', clone_url, tmpdir.strpath)
147 assert 'HTTP Error 404: Not Found' in stderr
148
149 def test_clone_git_with_slashes(self, rc_web_server, tmpdir):
150 clone_url = rc_web_server.repo_clone_url('//' + GIT_REPO)
151 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
152 assert 'not found' in stderr
153
144 154 def test_clone_existing_path_hg_not_in_database(
145 155 self, rc_web_server, tmpdir, fs_repo_only):
146 156
147 157 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
148 158 clone_url = rc_web_server.repo_clone_url(db_name)
149 159 stdout, stderr = Command('/tmp').execute(
150 160 'hg clone', clone_url, tmpdir.strpath)
151 161 assert 'HTTP Error 404: Not Found' in stderr
152 162
153 163 def test_clone_existing_path_git_not_in_database(
154 164 self, rc_web_server, tmpdir, fs_repo_only):
155 165 db_name = fs_repo_only('not-in-db-git', repo_type='git')
156 166 clone_url = rc_web_server.repo_clone_url(db_name)
157 167 stdout, stderr = Command('/tmp').execute(
158 168 'git clone', clone_url, tmpdir.strpath)
159 169 assert 'not found' in stderr
160 170
161 171 def test_clone_existing_path_hg_not_in_database_different_scm(
162 172 self, rc_web_server, tmpdir, fs_repo_only):
163 173 db_name = fs_repo_only('not-in-db-git', repo_type='git')
164 174 clone_url = rc_web_server.repo_clone_url(db_name)
165 175 stdout, stderr = Command('/tmp').execute(
166 176 'hg clone', clone_url, tmpdir.strpath)
167 177 assert 'HTTP Error 404: Not Found' in stderr
168 178
169 179 def test_clone_existing_path_git_not_in_database_different_scm(
170 180 self, rc_web_server, tmpdir, fs_repo_only):
171 181 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
172 182 clone_url = rc_web_server.repo_clone_url(db_name)
173 183 stdout, stderr = Command('/tmp').execute(
174 184 'git clone', clone_url, tmpdir.strpath)
175 185 assert 'not found' in stderr
176 186
177 187 def test_clone_non_existing_store_path_hg(self, rc_web_server, tmpdir, user_util):
178 188 repo = user_util.create_repo()
179 189 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
180 190
181 191 # Damage repo by removing it's folder
182 192 RepoModel()._delete_filesystem_repo(repo)
183 193
184 194 stdout, stderr = Command('/tmp').execute(
185 195 'hg clone', clone_url, tmpdir.strpath)
186 196 assert 'HTTP Error 404: Not Found' in stderr
187 197
188 198 def test_clone_non_existing_store_path_git(self, rc_web_server, tmpdir, user_util):
189 199 repo = user_util.create_repo(repo_type='git')
190 200 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
191 201
192 202 # Damage repo by removing it's folder
193 203 RepoModel()._delete_filesystem_repo(repo)
194 204
195 205 stdout, stderr = Command('/tmp').execute(
196 206 'git clone', clone_url, tmpdir.strpath)
197 207 assert 'not found' in stderr
198 208
199 209 def test_push_new_file_hg(self, rc_web_server, tmpdir):
200 210 clone_url = rc_web_server.repo_clone_url(HG_REPO)
201 211 stdout, stderr = Command('/tmp').execute(
202 212 'hg clone', clone_url, tmpdir.strpath)
203 213
204 214 stdout, stderr = _add_files_and_push(
205 215 'hg', tmpdir.strpath, clone_url=clone_url)
206 216
207 217 assert 'pushing to' in stdout
208 218 assert 'size summary' in stdout
209 219
210 220 def test_push_new_file_git(self, rc_web_server, tmpdir):
211 221 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
212 222 stdout, stderr = Command('/tmp').execute(
213 223 'git clone', clone_url, tmpdir.strpath)
214 224
215 225 # commit some stuff into this repo
216 226 stdout, stderr = _add_files_and_push(
217 227 'git', tmpdir.strpath, clone_url=clone_url)
218 228
219 229 _check_proper_git_push(stdout, stderr)
220 230
221 231 def test_push_invalidates_cache(self, rc_web_server, tmpdir):
222 232 hg_repo = Repository.get_by_repo_name(HG_REPO)
223 233
224 234 # init cache objects
225 235 CacheKey.delete_all_cache()
226 236 cache_namespace_uid = 'cache_push_test.{}'.format(hg_repo.repo_id)
227 237 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
228 238 repo_id=hg_repo.repo_id)
229 239
230 240 inv_context_manager = rc_cache.InvalidationContext(
231 241 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
232 242
233 243 with inv_context_manager as invalidation_context:
234 244 # __enter__ will create and register cache objects
235 245 pass
236 246
237 247 # clone to init cache
238 248 clone_url = rc_web_server.repo_clone_url(hg_repo.repo_name)
239 249 stdout, stderr = Command('/tmp').execute(
240 250 'hg clone', clone_url, tmpdir.strpath)
241 251
242 252 cache_keys = hg_repo.cache_keys
243 253 assert cache_keys != []
244 254 for key in cache_keys:
245 255 assert key.cache_active is True
246 256
247 257 # PUSH that should trigger invalidation cache
248 258 stdout, stderr = _add_files_and_push(
249 259 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
250 260
251 261 # flush...
252 262 Session().commit()
253 263 hg_repo = Repository.get_by_repo_name(HG_REPO)
254 264 cache_keys = hg_repo.cache_keys
255 265 assert cache_keys != []
256 266 for key in cache_keys:
257 267 # keys should be marked as not active
258 268 assert key.cache_active is False
259 269
260 270 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
261 271 clone_url = rc_web_server.repo_clone_url(HG_REPO)
262 272 stdout, stderr = Command('/tmp').execute(
263 273 'hg clone', clone_url, tmpdir.strpath)
264 274
265 275 push_url = rc_web_server.repo_clone_url(
266 276 HG_REPO, user='bad', passwd='name')
267 277 stdout, stderr = _add_files_and_push(
268 278 'hg', tmpdir.strpath, clone_url=push_url)
269 279
270 280 assert 'abort: authorization failed' in stderr
271 281
272 282 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
273 283 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
274 284 stdout, stderr = Command('/tmp').execute(
275 285 'git clone', clone_url, tmpdir.strpath)
276 286
277 287 push_url = rc_web_server.repo_clone_url(
278 288 GIT_REPO, user='bad', passwd='name')
279 289 stdout, stderr = _add_files_and_push(
280 290 'git', tmpdir.strpath, clone_url=push_url)
281 291
282 292 assert 'fatal: Authentication failed' in stderr
283 293
284 294 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
285 295 clone_url = rc_web_server.repo_clone_url(HG_REPO)
286 296 stdout, stderr = Command('/tmp').execute(
287 297 'hg clone', clone_url, tmpdir.strpath)
288 298
289 299 stdout, stderr = _add_files_and_push(
290 300 'hg', tmpdir.strpath,
291 301 clone_url=rc_web_server.repo_clone_url('not-existing'))
292 302
293 303 assert 'HTTP Error 404: Not Found' in stderr
294 304
295 305 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
296 306 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
297 307 stdout, stderr = Command('/tmp').execute(
298 308 'git clone', clone_url, tmpdir.strpath)
299 309
300 310 stdout, stderr = _add_files_and_push(
301 311 'git', tmpdir.strpath,
302 312 clone_url=rc_web_server.repo_clone_url('not-existing'))
303 313
304 314 assert 'not found' in stderr
305 315
306 316 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
307 317 user_model = UserModel()
308 318 try:
309 319 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
310 320 Session().commit()
311 321 time.sleep(2)
312 322 clone_url = rc_web_server.repo_clone_url(HG_REPO)
313 323 stdout, stderr = Command('/tmp').execute(
314 324 'hg clone', clone_url, tmpdir.strpath)
315 325 assert 'abort: HTTP Error 403: Forbidden' in stderr
316 326 finally:
317 327 # release IP restrictions
318 328 for ip in UserIpMap.getAll():
319 329 UserIpMap.delete(ip.ip_id)
320 330 Session().commit()
321 331
322 332 time.sleep(2)
323 333
324 334 stdout, stderr = Command('/tmp').execute(
325 335 'hg clone', clone_url, tmpdir.strpath)
326 336 _check_proper_clone(stdout, stderr, 'hg')
327 337
328 338 def test_ip_restriction_git(self, rc_web_server, tmpdir):
329 339 user_model = UserModel()
330 340 try:
331 341 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
332 342 Session().commit()
333 343 time.sleep(2)
334 344 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
335 345 stdout, stderr = Command('/tmp').execute(
336 346 'git clone', clone_url, tmpdir.strpath)
337 347 msg = "The requested URL returned error: 403"
338 348 assert msg in stderr
339 349 finally:
340 350 # release IP restrictions
341 351 for ip in UserIpMap.getAll():
342 352 UserIpMap.delete(ip.ip_id)
343 353 Session().commit()
344 354
345 355 time.sleep(2)
346 356
347 357 cmd = Command('/tmp')
348 358 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
349 359 cmd.assert_returncode_success()
350 360 _check_proper_clone(stdout, stderr, 'git')
351 361
352 362 def test_clone_by_auth_token(
353 363 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
354 364 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
355 365 'egg:rhodecode-enterprise-ce#rhodecode'])
356 366
357 367 user = user_util.create_user()
358 368 token = user.auth_tokens[1]
359 369
360 370 clone_url = rc_web_server.repo_clone_url(
361 371 HG_REPO, user=user.username, passwd=token)
362 372
363 373 stdout, stderr = Command('/tmp').execute(
364 374 'hg clone', clone_url, tmpdir.strpath)
365 375 _check_proper_clone(stdout, stderr, 'hg')
366 376
367 377 def test_clone_by_auth_token_expired(
368 378 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
369 379 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
370 380 'egg:rhodecode-enterprise-ce#rhodecode'])
371 381
372 382 user = user_util.create_user()
373 383 auth_token = AuthTokenModel().create(
374 384 user.user_id, 'test-token', -10, AuthTokenModel.cls.ROLE_VCS)
375 385 token = auth_token.api_key
376 386
377 387 clone_url = rc_web_server.repo_clone_url(
378 388 HG_REPO, user=user.username, passwd=token)
379 389
380 390 stdout, stderr = Command('/tmp').execute(
381 391 'hg clone', clone_url, tmpdir.strpath)
382 392 assert 'abort: authorization failed' in stderr
383 393
384 394 def test_clone_by_auth_token_bad_role(
385 395 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
386 396 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
387 397 'egg:rhodecode-enterprise-ce#rhodecode'])
388 398
389 399 user = user_util.create_user()
390 400 auth_token = AuthTokenModel().create(
391 401 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
392 402 token = auth_token.api_key
393 403
394 404 clone_url = rc_web_server.repo_clone_url(
395 405 HG_REPO, user=user.username, passwd=token)
396 406
397 407 stdout, stderr = Command('/tmp').execute(
398 408 'hg clone', clone_url, tmpdir.strpath)
399 409 assert 'abort: authorization failed' in stderr
400 410
401 411 def test_clone_by_auth_token_user_disabled(
402 412 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
403 413 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
404 414 'egg:rhodecode-enterprise-ce#rhodecode'])
405 415 user = user_util.create_user()
406 416 user.active = False
407 417 Session().add(user)
408 418 Session().commit()
409 419 token = user.auth_tokens[1]
410 420
411 421 clone_url = rc_web_server.repo_clone_url(
412 422 HG_REPO, user=user.username, passwd=token)
413 423
414 424 stdout, stderr = Command('/tmp').execute(
415 425 'hg clone', clone_url, tmpdir.strpath)
416 426 assert 'abort: authorization failed' in stderr
417 427
418 428 def test_clone_by_auth_token_with_scope(
419 429 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
420 430 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
421 431 'egg:rhodecode-enterprise-ce#rhodecode'])
422 432 user = user_util.create_user()
423 433 auth_token = AuthTokenModel().create(
424 434 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
425 435 token = auth_token.api_key
426 436
427 437 # manually set scope
428 438 auth_token.repo = Repository.get_by_repo_name(HG_REPO)
429 439 Session().add(auth_token)
430 440 Session().commit()
431 441
432 442 clone_url = rc_web_server.repo_clone_url(
433 443 HG_REPO, user=user.username, passwd=token)
434 444
435 445 stdout, stderr = Command('/tmp').execute(
436 446 'hg clone', clone_url, tmpdir.strpath)
437 447 _check_proper_clone(stdout, stderr, 'hg')
438 448
439 449 def test_clone_by_auth_token_with_wrong_scope(
440 450 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
441 451 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
442 452 'egg:rhodecode-enterprise-ce#rhodecode'])
443 453 user = user_util.create_user()
444 454 auth_token = AuthTokenModel().create(
445 455 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
446 456 token = auth_token.api_key
447 457
448 458 # manually set scope
449 459 auth_token.repo = Repository.get_by_repo_name(GIT_REPO)
450 460 Session().add(auth_token)
451 461 Session().commit()
452 462
453 463 clone_url = rc_web_server.repo_clone_url(
454 464 HG_REPO, user=user.username, passwd=token)
455 465
456 466 stdout, stderr = Command('/tmp').execute(
457 467 'hg clone', clone_url, tmpdir.strpath)
458 468 assert 'abort: authorization failed' in stderr
459 469
General Comments 0
You need to be logged in to leave comments. Login now