##// END OF EJS Templates
vcs: Use correct repo name attribute and remove workaround.
Martin Bornhold -
r898:b19bad01 default
parent child Browse files
Show More
@@ -1,510 +1,505 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import logging
28 28 import importlib
29 29 import re
30 30 from functools import wraps
31 31
32 32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 33 from webob.exc import (
34 34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35 35
36 36 import rhodecode
37 37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
40 40 from rhodecode.lib.exceptions import (
41 41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 42 NotAllowedToCreateUserError)
43 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 44 from rhodecode.lib.middleware import appenlight
45 45 from rhodecode.lib.middleware.utils import scm_app
46 46 from rhodecode.lib.utils import (
47 47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
48 48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
49 49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 50 from rhodecode.lib.vcs.backends import base
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import User, Repository, PullRequest
53 53 from rhodecode.model.scm import ScmModel
54 54 from rhodecode.model.pull_request import PullRequestModel
55 55
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 def initialize_generator(factory):
61 61 """
62 62 Initializes the returned generator by draining its first element.
63 63
64 64 This can be used to give a generator an initializer, which is the code
65 65 up to the first yield statement. This decorator enforces that the first
66 66 produced element has the value ``"__init__"`` to make its special
67 67 purpose very explicit in the using code.
68 68 """
69 69
70 70 @wraps(factory)
71 71 def wrapper(*args, **kwargs):
72 72 gen = factory(*args, **kwargs)
73 73 try:
74 74 init = gen.next()
75 75 except StopIteration:
76 76 raise ValueError('Generator must yield at least one element.')
77 77 if init != "__init__":
78 78 raise ValueError('First yielded element must be "__init__".')
79 79 return gen
80 80 return wrapper
81 81
82 82
83 83 class SimpleVCS(object):
84 84 """Common functionality for SCM HTTP handlers."""
85 85
86 86 SCM = 'unknown'
87 87
88 88 acl_repo_name = None
89 89 url_repo_name = None
90 90 vcs_repo_name = None
91 91
92 92 def __init__(self, application, config, registry):
93 93 self.registry = registry
94 94 self.application = application
95 95 self.config = config
96 96 # re-populated by specialized middleware
97 97 self.repo_vcs_config = base.Config()
98 98
99 99 # base path of repo locations
100 100 self.basepath = get_rhodecode_base_path()
101 101 # authenticate this VCS request using authfunc
102 102 auth_ret_code_detection = \
103 103 str2bool(self.config.get('auth_ret_code_detection', False))
104 104 self.authenticate = BasicAuth(
105 105 '', authenticate, registry, config.get('auth_ret_code'),
106 106 auth_ret_code_detection)
107 107 self.ip_addr = '0.0.0.0'
108 108
109 109 def set_repo_names(self, environ):
110 110 """
111 111 This will populate the attributes acl_repo_name, url_repo_name,
112 112 vcs_repo_name and pr_id on the current instance.
113 113 """
114 114 # TODO: martinb: Move to class or module scope.
115 115 # TODO: martinb: Check if we have to use re.UNICODE.
116 116 # TODO: martinb: Check which chars are allowed for repo/group names.
117 117 # These chars are excluded: '`?=[]\;\'"<>,/~!@#$%^&*()+{}|: '
118 118 # Code from: rhodecode/lib/utils.py:repo_name_slug()
119 119 pr_regex = re.compile(
120 120 '(?P<base_name>(?:[\w-]+)(?:/[\w-]+)*)/' # repo groups
121 121 '(?P<repo_name>[\w-]+)' # target repo name
122 122 '/pull-request/(?P<pr_id>\d+)/repository') # pr suffix
123 123
124 124 # Get url repo name from environment.
125 125 self.url_repo_name = self._get_repository_name(environ)
126 126
127 127 # Check if this is a request to a shadow repository. In case of a
128 128 # shadow repo set vcs_repo_name to the file system path pointing to the
129 129 # shadow repo. And set acl_repo_name to the pull request target repo
130 130 # because we use the target repo for permission checks. Otherwise all
131 131 # names are equal.
132 132 match = pr_regex.match(self.url_repo_name)
133 133 if match:
134 134 # Get pull request instance.
135 135 match_dict = match.groupdict()
136 136 pr_id = match_dict['pr_id']
137 137 pull_request = PullRequest.get(pr_id)
138 138
139 139 # Get file system path to shadow repository.
140 140 workspace_id = PullRequestModel()._workspace_id(pull_request)
141 141 target_vcs = pull_request.target_repo.scm_instance()
142 142 vcs_repo_name = target_vcs._get_shadow_repository_path(
143 143 workspace_id)
144 144
145 145 # Store names for later usage.
146 146 self.pr_id = pr_id
147 147 self.vcs_repo_name = vcs_repo_name
148 148 self.acl_repo_name = pull_request.target_repo.repo_name
149 149 else:
150 150 # All names are equal for normal (non shadow) repositories.
151 151 self.acl_repo_name = self.url_repo_name
152 152 self.vcs_repo_name = self.url_repo_name
153 153 self.pr_id = None
154 154
155 155 @property
156 def repo_name(self):
157 # TODO: johbo: Remove, switch to correct repo name attribute
158 return self.acl_repo_name
159
160 @property
161 156 def scm_app(self):
162 157 custom_implementation = self.config.get('vcs.scm_app_implementation')
163 158 if custom_implementation and custom_implementation != 'pyro4':
164 159 log.info(
165 160 "Using custom implementation of scm_app: %s",
166 161 custom_implementation)
167 162 scm_app_impl = importlib.import_module(custom_implementation)
168 163 else:
169 164 scm_app_impl = scm_app
170 165 return scm_app_impl
171 166
172 167 def _get_by_id(self, repo_name):
173 168 """
174 169 Gets a special pattern _<ID> from clone url and tries to replace it
175 170 with a repository_name for support of _<ID> non changeable urls
176 171 """
177 172
178 173 data = repo_name.split('/')
179 174 if len(data) >= 2:
180 175 from rhodecode.model.repo import RepoModel
181 176 by_id_match = RepoModel().get_repo_by_id(repo_name)
182 177 if by_id_match:
183 178 data[1] = by_id_match.repo_name
184 179
185 180 return safe_str('/'.join(data))
186 181
187 182 def _invalidate_cache(self, repo_name):
188 183 """
189 184 Set's cache for this repository for invalidation on next access
190 185
191 186 :param repo_name: full repo name, also a cache key
192 187 """
193 188 ScmModel().mark_for_invalidation(repo_name)
194 189
195 190 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
196 191 db_repo = Repository.get_by_repo_name(repo_name)
197 192 if not db_repo:
198 193 log.debug('Repository `%s` not found inside the database.',
199 194 repo_name)
200 195 return False
201 196
202 197 if db_repo.repo_type != scm_type:
203 198 log.warning(
204 199 'Repository `%s` have incorrect scm_type, expected %s got %s',
205 200 repo_name, db_repo.repo_type, scm_type)
206 201 return False
207 202
208 203 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
209 204
210 205 def valid_and_active_user(self, user):
211 206 """
212 207 Checks if that user is not empty, and if it's actually object it checks
213 208 if he's active.
214 209
215 210 :param user: user object or None
216 211 :return: boolean
217 212 """
218 213 if user is None:
219 214 return False
220 215
221 216 elif user.active:
222 217 return True
223 218
224 219 return False
225 220
226 221 def _check_permission(self, action, user, repo_name, ip_addr=None):
227 222 """
228 223 Checks permissions using action (push/pull) user and repository
229 224 name
230 225
231 226 :param action: push or pull action
232 227 :param user: user instance
233 228 :param repo_name: repository name
234 229 """
235 230 # check IP
236 231 inherit = user.inherit_default_permissions
237 232 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
238 233 inherit_from_default=inherit)
239 234 if ip_allowed:
240 235 log.info('Access for IP:%s allowed', ip_addr)
241 236 else:
242 237 return False
243 238
244 239 if action == 'push':
245 240 if not HasPermissionAnyMiddleware('repository.write',
246 241 'repository.admin')(user,
247 242 repo_name):
248 243 return False
249 244
250 245 else:
251 246 # any other action need at least read permission
252 247 if not HasPermissionAnyMiddleware('repository.read',
253 248 'repository.write',
254 249 'repository.admin')(user,
255 250 repo_name):
256 251 return False
257 252
258 253 return True
259 254
260 255 def _check_ssl(self, environ, start_response):
261 256 """
262 257 Checks the SSL check flag and returns False if SSL is not present
263 258 and required True otherwise
264 259 """
265 260 org_proto = environ['wsgi._org_proto']
266 261 # check if we have SSL required ! if not it's a bad request !
267 262 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
268 263 if require_ssl and org_proto == 'http':
269 264 log.debug('proto is %s and SSL is required BAD REQUEST !',
270 265 org_proto)
271 266 return False
272 267 return True
273 268
274 269 def __call__(self, environ, start_response):
275 270 try:
276 271 return self._handle_request(environ, start_response)
277 272 except Exception:
278 273 log.exception("Exception while handling request")
279 274 appenlight.track_exception(environ)
280 275 return HTTPInternalServerError()(environ, start_response)
281 276 finally:
282 277 meta.Session.remove()
283 278
284 279 def _handle_request(self, environ, start_response):
285 280
286 281 if not self._check_ssl(environ, start_response):
287 282 reason = ('SSL required, while RhodeCode was unable '
288 283 'to detect this as SSL request')
289 284 log.debug('User not allowed to proceed, %s', reason)
290 285 return HTTPNotAcceptable(reason)(environ, start_response)
291 286
292 if not self.repo_name:
293 log.warning('Repository name is empty: %s', self.repo_name)
287 if not self.url_repo_name:
288 log.warning('Repository name is empty: %s', self.url_repo_name)
294 289 # failed to get repo name, we fail now
295 290 return HTTPNotFound()(environ, start_response)
296 log.debug('Extracted repo name is %s', self.repo_name)
291 log.debug('Extracted repo name is %s', self.url_repo_name)
297 292
298 293 ip_addr = get_ip_addr(environ)
299 294 username = None
300 295
301 296 # skip passing error to error controller
302 297 environ['pylons.status_code_redirect'] = True
303 298
304 299 # ======================================================================
305 300 # GET ACTION PULL or PUSH
306 301 # ======================================================================
307 302 action = self._get_action(environ)
308 303
309 304 # ======================================================================
310 305 # Check if this is a request to a shadow repository of a pull request.
311 306 # In this case only pull action is allowed.
312 307 # ======================================================================
313 308 if self.pr_id is not None and action != 'pull':
314 309 reason = 'Only pull action is allowed for shadow repositories.'
315 310 log.debug('User not allowed to proceed, %s', reason)
316 311 return HTTPNotAcceptable(reason)(environ, start_response)
317 312
318 313 # ======================================================================
319 314 # CHECK ANONYMOUS PERMISSION
320 315 # ======================================================================
321 316 if action in ['pull', 'push']:
322 317 anonymous_user = User.get_default_user()
323 318 username = anonymous_user.username
324 319 if anonymous_user.active:
325 320 # ONLY check permissions if the user is activated
326 321 anonymous_perm = self._check_permission(
327 action, anonymous_user, self.repo_name, ip_addr)
322 action, anonymous_user, self.acl_repo_name, ip_addr)
328 323 else:
329 324 anonymous_perm = False
330 325
331 326 if not anonymous_user.active or not anonymous_perm:
332 327 if not anonymous_user.active:
333 328 log.debug('Anonymous access is disabled, running '
334 329 'authentication')
335 330
336 331 if not anonymous_perm:
337 332 log.debug('Not enough credentials to access this '
338 333 'repository as anonymous user')
339 334
340 335 username = None
341 336 # ==============================================================
342 337 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
343 338 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
344 339 # ==============================================================
345 340
346 341 # try to auth based on environ, container auth methods
347 342 log.debug('Running PRE-AUTH for container based authentication')
348 343 pre_auth = authenticate(
349 344 '', '', environ, VCS_TYPE, registry=self.registry)
350 345 if pre_auth and pre_auth.get('username'):
351 346 username = pre_auth['username']
352 347 log.debug('PRE-AUTH got %s as username', username)
353 348
354 349 # If not authenticated by the container, running basic auth
355 350 if not username:
356 351 self.authenticate.realm = get_rhodecode_realm()
357 352
358 353 try:
359 354 result = self.authenticate(environ)
360 355 except (UserCreationError, NotAllowedToCreateUserError) as e:
361 356 log.error(e)
362 357 reason = safe_str(e)
363 358 return HTTPNotAcceptable(reason)(environ, start_response)
364 359
365 360 if isinstance(result, str):
366 361 AUTH_TYPE.update(environ, 'basic')
367 362 REMOTE_USER.update(environ, result)
368 363 username = result
369 364 else:
370 365 return result.wsgi_application(environ, start_response)
371 366
372 367 # ==============================================================
373 368 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
374 369 # ==============================================================
375 370 user = User.get_by_username(username)
376 371 if not self.valid_and_active_user(user):
377 372 return HTTPForbidden()(environ, start_response)
378 373 username = user.username
379 374 user.update_lastactivity()
380 375 meta.Session().commit()
381 376
382 377 # check user attributes for password change flag
383 378 user_obj = user
384 379 if user_obj and user_obj.username != User.DEFAULT_USER and \
385 380 user_obj.user_data.get('force_password_change'):
386 381 reason = 'password change required'
387 382 log.debug('User not allowed to authenticate, %s', reason)
388 383 return HTTPNotAcceptable(reason)(environ, start_response)
389 384
390 385 # check permissions for this repository
391 386 perm = self._check_permission(
392 action, user, self.repo_name, ip_addr)
387 action, user, self.acl_repo_name, ip_addr)
393 388 if not perm:
394 389 return HTTPForbidden()(environ, start_response)
395 390
396 391 # extras are injected into UI object and later available
397 392 # in hooks executed by rhodecode
398 393 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
399 394 extras = vcs_operation_context(
400 environ, repo_name=self.repo_name, username=username,
395 environ, repo_name=self.acl_repo_name, username=username,
401 396 action=action, scm=self.SCM,
402 397 check_locking=check_locking)
403 398
404 399 # ======================================================================
405 400 # REQUEST HANDLING
406 401 # ======================================================================
407 str_repo_name = safe_str(self.repo_name)
402 str_repo_name = safe_str(self.url_repo_name)
408 403 repo_path = os.path.join(
409 404 safe_str(self.basepath), safe_str(self.vcs_repo_name))
410 405 log.debug('Repository path is %s', repo_path)
411 406
412 407 fix_PATH()
413 408
414 409 log.info(
415 410 '%s action on %s repo "%s" by "%s" from %s',
416 411 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
417 412
418 413 return self._generate_vcs_response(
419 414 environ, start_response, repo_path, self.url_repo_name, extras, action)
420 415
421 416 @initialize_generator
422 417 def _generate_vcs_response(
423 418 self, environ, start_response, repo_path, repo_name, extras,
424 419 action):
425 420 """
426 421 Returns a generator for the response content.
427 422
428 423 This method is implemented as a generator, so that it can trigger
429 424 the cache validation after all content sent back to the client. It
430 425 also handles the locking exceptions which will be triggered when
431 426 the first chunk is produced by the underlying WSGI application.
432 427 """
433 428 callback_daemon, extras = self._prepare_callback_daemon(extras)
434 429 config = self._create_config(extras, self.acl_repo_name)
435 430 log.debug('HOOKS extras is %s', extras)
436 431 app = self._create_wsgi_app(repo_path, repo_name, config)
437 432
438 433 try:
439 434 with callback_daemon:
440 435 try:
441 436 response = app(environ, start_response)
442 437 finally:
443 438 # This statement works together with the decorator
444 439 # "initialize_generator" above. The decorator ensures that
445 440 # we hit the first yield statement before the generator is
446 441 # returned back to the WSGI server. This is needed to
447 442 # ensure that the call to "app" above triggers the
448 443 # needed callback to "start_response" before the
449 444 # generator is actually used.
450 445 yield "__init__"
451 446
452 447 for chunk in response:
453 448 yield chunk
454 449 except Exception as exc:
455 450 # TODO: johbo: Improve "translating" back the exception.
456 451 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
457 452 exc = HTTPLockedRC(*exc.args)
458 453 _code = rhodecode.CONFIG.get('lock_ret_code')
459 454 log.debug('Repository LOCKED ret code %s!', (_code,))
460 455 elif getattr(exc, '_vcs_kind', None) == 'requirement':
461 456 log.debug(
462 457 'Repository requires features unknown to this Mercurial')
463 458 exc = HTTPRequirementError(*exc.args)
464 459 else:
465 460 raise
466 461
467 462 for chunk in exc(environ, start_response):
468 463 yield chunk
469 464 finally:
470 465 # invalidate cache on push
471 466 try:
472 467 if action == 'push':
473 468 self._invalidate_cache(repo_name)
474 469 finally:
475 470 meta.Session.remove()
476 471
477 472 def _get_repository_name(self, environ):
478 473 """Get repository name out of the environmnent
479 474
480 475 :param environ: WSGI environment
481 476 """
482 477 raise NotImplementedError()
483 478
484 479 def _get_action(self, environ):
485 480 """Map request commands into a pull or push command.
486 481
487 482 :param environ: WSGI environment
488 483 """
489 484 raise NotImplementedError()
490 485
491 486 def _create_wsgi_app(self, repo_path, repo_name, config):
492 487 """Return the WSGI app that will finally handle the request."""
493 488 raise NotImplementedError()
494 489
495 490 def _create_config(self, extras, repo_name):
496 491 """Create a Pyro safe config representation."""
497 492 raise NotImplementedError()
498 493
499 494 def _prepare_callback_daemon(self, extras):
500 495 return prepare_callback_daemon(
501 496 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
502 497 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
503 498
504 499
505 500 def _should_check_locking(query_string):
506 501 # this is kind of hacky, but due to how mercurial handles client-server
507 502 # server see all operation on commit; bookmarks, phases and
508 503 # obsolescence marker in different transaction, we don't want to check
509 504 # locking on those
510 505 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now