##// END OF EJS Templates
simplevcs: store rc_extras reference inside the apps itself so it can be used during...
marcink -
r2389:0272fdf5 default
parent child Browse files
Show More
@@ -1,169 +1,171 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 import logging
22 22 import urllib
23 23 from urlparse import urljoin
24 24
25 25
26 26 import requests
27 27 from webob.exc import HTTPNotAcceptable
28 28
29 29 from rhodecode.lib.middleware import simplevcs
30 30 from rhodecode.lib.utils import is_valid_repo
31 31 from rhodecode.lib.utils2 import str2bool
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class SimpleSvnApp(object):
37 37 IGNORED_HEADERS = [
38 38 'connection', 'keep-alive', 'content-encoding',
39 39 'transfer-encoding', 'content-length']
40 rc_extras = {}
41
40 42
41 43 def __init__(self, config):
42 44 self.config = config
43 45
44 46 def __call__(self, environ, start_response):
45 47 request_headers = self._get_request_headers(environ)
46 48
47 49 data = environ['wsgi.input']
48 50 # johbo: Avoid that we end up with sending the request in chunked
49 51 # transfer encoding (mainly on Gunicorn). If we know the content
50 52 # length, then we should transfer the payload in one request.
51 53 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
52 54 data = data.read()
53 55
54 56 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
55 57 self._get_url(environ['PATH_INFO']))
56 58 response = requests.request(
57 59 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
58 60 data=data, headers=request_headers)
59 61
60 62 if response.status_code not in [200, 401]:
61 63 if response.status_code >= 500:
62 64 log.error('Got SVN response:%s with text:`%s`',
63 65 response, response.text)
64 66 else:
65 67 log.debug('Got SVN response:%s with text:`%s`',
66 68 response, response.text)
67 69
68 70 response_headers = self._get_response_headers(response.headers)
69 71 start_response(
70 72 '{} {}'.format(response.status_code, response.reason),
71 73 response_headers)
72 74 return response.iter_content(chunk_size=1024)
73 75
74 76 def _get_url(self, path):
75 77 url_path = urljoin(
76 78 self.config.get('subversion_http_server_url', ''), path)
77 79 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
78 80 return url_path
79 81
80 82 def _get_request_headers(self, environ):
81 83 headers = {}
82 84
83 85 for key in environ:
84 86 if not key.startswith('HTTP_'):
85 87 continue
86 88 new_key = key.split('_')
87 89 new_key = [k.capitalize() for k in new_key[1:]]
88 90 new_key = '-'.join(new_key)
89 91 headers[new_key] = environ[key]
90 92
91 93 if 'CONTENT_TYPE' in environ:
92 94 headers['Content-Type'] = environ['CONTENT_TYPE']
93 95
94 96 if 'CONTENT_LENGTH' in environ:
95 97 headers['Content-Length'] = environ['CONTENT_LENGTH']
96 98
97 99 return headers
98 100
99 101 def _get_response_headers(self, headers):
100 102 headers = [
101 103 (h, headers[h])
102 104 for h in headers
103 105 if h.lower() not in self.IGNORED_HEADERS
104 106 ]
105 107
106 108 return headers
107 109
108 110
109 111 class DisabledSimpleSvnApp(object):
110 112 def __init__(self, config):
111 113 self.config = config
112 114
113 115 def __call__(self, environ, start_response):
114 116 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
115 117 log.warning(reason)
116 118 return HTTPNotAcceptable(reason)(environ, start_response)
117 119
118 120
119 121 class SimpleSvn(simplevcs.SimpleVCS):
120 122
121 123 SCM = 'svn'
122 124 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
123 125 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
124 126
125 127 def _get_repository_name(self, environ):
126 128 """
127 129 Gets repository name out of PATH_INFO header
128 130
129 131 :param environ: environ where PATH_INFO is stored
130 132 """
131 133 path = environ['PATH_INFO'].split('!')
132 134 repo_name = path[0].strip('/')
133 135
134 136 # SVN includes the whole path in it's requests, including
135 137 # subdirectories inside the repo. Therefore we have to search for
136 138 # the repo root directory.
137 139 if not is_valid_repo(repo_name, self.base_path, self.SCM):
138 140 current_path = ''
139 141 for component in repo_name.split('/'):
140 142 current_path += component
141 143 if is_valid_repo(current_path, self.base_path, self.SCM):
142 144 return current_path
143 145 current_path += '/'
144 146
145 147 return repo_name
146 148
147 149 def _get_action(self, environ):
148 150 return (
149 151 'pull'
150 152 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
151 153 else 'push')
152 154
153 155 def _create_wsgi_app(self, repo_path, repo_name, config):
154 156 if self._is_svn_enabled():
155 157 return SimpleSvnApp(config)
156 158 # we don't have http proxy enabled return dummy request handler
157 159 return DisabledSimpleSvnApp(config)
158 160
159 161 def _is_svn_enabled(self):
160 162 conf = self.repo_vcs_config
161 163 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
162 164
163 165 def _create_config(self, extras, repo_name):
164 166 conf = self.repo_vcs_config
165 167 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
166 168 server_url = server_url or self.DEFAULT_HTTP_SERVER
167 169
168 170 extras['subversion_http_server_url'] = server_url
169 171 return extras
@@ -1,610 +1,612 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31
32 32 import time
33 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 34 # TODO(marcink): check if we should use webob.exc here ?
35 35 from pyramid.httpexceptions import (
36 36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 37
38 38 import rhodecode
39 39 from rhodecode.authentication.base import (
40 40 authenticate, get_perms_cache_manager, VCS_TYPE)
41 41 from rhodecode.lib import caches
42 42 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
43 43 from rhodecode.lib.base import (
44 44 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
45 45 from rhodecode.lib.exceptions import (
46 46 HTTPLockedRC, HTTPRequirementError, UserCreationError,
47 47 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 from rhodecode.model import meta
56 56 from rhodecode.model.db import User, Repository, PullRequest
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.pull_request import PullRequestModel
59 59 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 def initialize_generator(factory):
65 65 """
66 66 Initializes the returned generator by draining its first element.
67 67
68 68 This can be used to give a generator an initializer, which is the code
69 69 up to the first yield statement. This decorator enforces that the first
70 70 produced element has the value ``"__init__"`` to make its special
71 71 purpose very explicit in the using code.
72 72 """
73 73
74 74 @wraps(factory)
75 75 def wrapper(*args, **kwargs):
76 76 gen = factory(*args, **kwargs)
77 77 try:
78 78 init = gen.next()
79 79 except StopIteration:
80 80 raise ValueError('Generator must yield at least one element.')
81 81 if init != "__init__":
82 82 raise ValueError('First yielded element must be "__init__".')
83 83 return gen
84 84 return wrapper
85 85
86 86
87 87 class SimpleVCS(object):
88 88 """Common functionality for SCM HTTP handlers."""
89 89
90 90 SCM = 'unknown'
91 91
92 92 acl_repo_name = None
93 93 url_repo_name = None
94 94 vcs_repo_name = None
95 rc_extras = {}
95 96
96 97 # We have to handle requests to shadow repositories different than requests
97 98 # to normal repositories. Therefore we have to distinguish them. To do this
98 99 # we use this regex which will match only on URLs pointing to shadow
99 100 # repositories.
100 101 shadow_repo_re = re.compile(
101 102 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
102 103 '(?P<target>{slug_pat})/' # target repo
103 104 'pull-request/(?P<pr_id>\d+)/' # pull request
104 105 'repository$' # shadow repo
105 106 .format(slug_pat=SLUG_RE.pattern))
106 107
107 108 def __init__(self, config, registry):
108 109 self.registry = registry
109 110 self.config = config
110 111 # re-populated by specialized middleware
111 112 self.repo_vcs_config = base.Config()
112 113 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
113 114
114 115 registry.rhodecode_settings = self.rhodecode_settings
115 116 # authenticate this VCS request using authfunc
116 117 auth_ret_code_detection = \
117 118 str2bool(self.config.get('auth_ret_code_detection', False))
118 119 self.authenticate = BasicAuth(
119 120 '', authenticate, registry, config.get('auth_ret_code'),
120 121 auth_ret_code_detection)
121 122 self.ip_addr = '0.0.0.0'
122 123
123 124 @property
124 125 def base_path(self):
125 126 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
126 127 if not settings_path:
127 128 # try, maybe we passed in explicitly as config option
128 129 settings_path = self.config.get('base_path')
129 130 return settings_path
130 131
131 132 def set_repo_names(self, environ):
132 133 """
133 134 This will populate the attributes acl_repo_name, url_repo_name,
134 135 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
135 136 shadow) repositories all names are equal. In case of requests to a
136 137 shadow repository the acl-name points to the target repo of the pull
137 138 request and the vcs-name points to the shadow repo file system path.
138 139 The url-name is always the URL used by the vcs client program.
139 140
140 141 Example in case of a shadow repo:
141 142 acl_repo_name = RepoGroup/MyRepo
142 143 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
143 144 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
144 145 """
145 146 # First we set the repo name from URL for all attributes. This is the
146 147 # default if handling normal (non shadow) repo requests.
147 148 self.url_repo_name = self._get_repository_name(environ)
148 149 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
149 150 self.is_shadow_repo = False
150 151
151 152 # Check if this is a request to a shadow repository.
152 153 match = self.shadow_repo_re.match(self.url_repo_name)
153 154 if match:
154 155 match_dict = match.groupdict()
155 156
156 157 # Build acl repo name from regex match.
157 158 acl_repo_name = safe_unicode('{groups}{target}'.format(
158 159 groups=match_dict['groups'] or '',
159 160 target=match_dict['target']))
160 161
161 162 # Retrieve pull request instance by ID from regex match.
162 163 pull_request = PullRequest.get(match_dict['pr_id'])
163 164
164 165 # Only proceed if we got a pull request and if acl repo name from
165 166 # URL equals the target repo name of the pull request.
166 167 if pull_request and (acl_repo_name ==
167 168 pull_request.target_repo.repo_name):
168 169 # Get file system path to shadow repository.
169 170 workspace_id = PullRequestModel()._workspace_id(pull_request)
170 171 target_vcs = pull_request.target_repo.scm_instance()
171 172 vcs_repo_name = target_vcs._get_shadow_repository_path(
172 173 workspace_id)
173 174
174 175 # Store names for later usage.
175 176 self.vcs_repo_name = vcs_repo_name
176 177 self.acl_repo_name = acl_repo_name
177 178 self.is_shadow_repo = True
178 179
179 180 log.debug('Setting all VCS repository names: %s', {
180 181 'acl_repo_name': self.acl_repo_name,
181 182 'url_repo_name': self.url_repo_name,
182 183 'vcs_repo_name': self.vcs_repo_name,
183 184 })
184 185
185 186 @property
186 187 def scm_app(self):
187 188 custom_implementation = self.config['vcs.scm_app_implementation']
188 189 if custom_implementation == 'http':
189 190 log.info('Using HTTP implementation of scm app.')
190 191 scm_app_impl = scm_app_http
191 192 else:
192 193 log.info('Using custom implementation of scm_app: "{}"'.format(
193 194 custom_implementation))
194 195 scm_app_impl = importlib.import_module(custom_implementation)
195 196 return scm_app_impl
196 197
197 198 def _get_by_id(self, repo_name):
198 199 """
199 200 Gets a special pattern _<ID> from clone url and tries to replace it
200 201 with a repository_name for support of _<ID> non changeable urls
201 202 """
202 203
203 204 data = repo_name.split('/')
204 205 if len(data) >= 2:
205 206 from rhodecode.model.repo import RepoModel
206 207 by_id_match = RepoModel().get_repo_by_id(repo_name)
207 208 if by_id_match:
208 209 data[1] = by_id_match.repo_name
209 210
210 211 return safe_str('/'.join(data))
211 212
212 213 def _invalidate_cache(self, repo_name):
213 214 """
214 215 Set's cache for this repository for invalidation on next access
215 216
216 217 :param repo_name: full repo name, also a cache key
217 218 """
218 219 ScmModel().mark_for_invalidation(repo_name)
219 220
220 221 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
221 222 db_repo = Repository.get_by_repo_name(repo_name)
222 223 if not db_repo:
223 224 log.debug('Repository `%s` not found inside the database.',
224 225 repo_name)
225 226 return False
226 227
227 228 if db_repo.repo_type != scm_type:
228 229 log.warning(
229 230 'Repository `%s` have incorrect scm_type, expected %s got %s',
230 231 repo_name, db_repo.repo_type, scm_type)
231 232 return False
232 233
233 234 return is_valid_repo(repo_name, base_path,
234 235 explicit_scm=scm_type, expect_scm=scm_type)
235 236
236 237 def valid_and_active_user(self, user):
237 238 """
238 239 Checks if that user is not empty, and if it's actually object it checks
239 240 if he's active.
240 241
241 242 :param user: user object or None
242 243 :return: boolean
243 244 """
244 245 if user is None:
245 246 return False
246 247
247 248 elif user.active:
248 249 return True
249 250
250 251 return False
251 252
252 253 @property
253 254 def is_shadow_repo_dir(self):
254 255 return os.path.isdir(self.vcs_repo_name)
255 256
256 257 def _check_permission(self, action, user, repo_name, ip_addr=None,
257 258 plugin_id='', plugin_cache_active=False, cache_ttl=0):
258 259 """
259 260 Checks permissions using action (push/pull) user and repository
260 261 name. If plugin_cache and ttl is set it will use the plugin which
261 262 authenticated the user to store the cached permissions result for N
262 263 amount of seconds as in cache_ttl
263 264
264 265 :param action: push or pull action
265 266 :param user: user instance
266 267 :param repo_name: repository name
267 268 """
268 269
269 270 # get instance of cache manager configured for a namespace
270 271 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
271 272 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
272 273 plugin_id, plugin_cache_active, cache_ttl)
273 274
274 275 # for environ based password can be empty, but then the validation is
275 276 # on the server that fills in the env data needed for authentication
276 277 _perm_calc_hash = caches.compute_key_from_params(
277 278 plugin_id, action, user.user_id, repo_name, ip_addr)
278 279
279 280 # _authenticate is a wrapper for .auth() method of plugin.
280 281 # it checks if .auth() sends proper data.
281 282 # For RhodeCodeExternalAuthPlugin it also maps users to
282 283 # Database and maps the attributes returned from .auth()
283 284 # to RhodeCode database. If this function returns data
284 285 # then auth is correct.
285 286 start = time.time()
286 287 log.debug('Running plugin `%s` permissions check', plugin_id)
287 288
288 289 def perm_func():
289 290 """
290 291 This function is used internally in Cache of Beaker to calculate
291 292 Results
292 293 """
293 294 log.debug('auth: calculating permission access now...')
294 295 # check IP
295 296 inherit = user.inherit_default_permissions
296 297 ip_allowed = AuthUser.check_ip_allowed(
297 298 user.user_id, ip_addr, inherit_from_default=inherit)
298 299 if ip_allowed:
299 300 log.info('Access for IP:%s allowed', ip_addr)
300 301 else:
301 302 return False
302 303
303 304 if action == 'push':
304 305 perms = ('repository.write', 'repository.admin')
305 306 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
306 307 return False
307 308
308 309 else:
309 310 # any other action need at least read permission
310 311 perms = (
311 312 'repository.read', 'repository.write', 'repository.admin')
312 313 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
313 314 return False
314 315
315 316 return True
316 317
317 318 if plugin_cache_active:
318 319 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
319 320 perm_result = cache_manager.get(
320 321 _perm_calc_hash, createfunc=perm_func)
321 322 else:
322 323 perm_result = perm_func()
323 324
324 325 auth_time = time.time() - start
325 326 log.debug('Permissions for plugin `%s` completed in %.3fs, '
326 327 'expiration time of fetched cache %.1fs.',
327 328 plugin_id, auth_time, cache_ttl)
328 329
329 330 return perm_result
330 331
331 332 def _check_ssl(self, environ, start_response):
332 333 """
333 334 Checks the SSL check flag and returns False if SSL is not present
334 335 and required True otherwise
335 336 """
336 337 org_proto = environ['wsgi._org_proto']
337 338 # check if we have SSL required ! if not it's a bad request !
338 339 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
339 340 if require_ssl and org_proto == 'http':
340 341 log.debug('proto is %s and SSL is required BAD REQUEST !',
341 342 org_proto)
342 343 return False
343 344 return True
344 345
345 346 def __call__(self, environ, start_response):
346 347 try:
347 348 return self._handle_request(environ, start_response)
348 349 except Exception:
349 350 log.exception("Exception while handling request")
350 351 appenlight.track_exception(environ)
351 352 return HTTPInternalServerError()(environ, start_response)
352 353 finally:
353 354 meta.Session.remove()
354 355
355 356 def _handle_request(self, environ, start_response):
356 357
357 358 if not self._check_ssl(environ, start_response):
358 359 reason = ('SSL required, while RhodeCode was unable '
359 360 'to detect this as SSL request')
360 361 log.debug('User not allowed to proceed, %s', reason)
361 362 return HTTPNotAcceptable(reason)(environ, start_response)
362 363
363 364 if not self.url_repo_name:
364 365 log.warning('Repository name is empty: %s', self.url_repo_name)
365 366 # failed to get repo name, we fail now
366 367 return HTTPNotFound()(environ, start_response)
367 368 log.debug('Extracted repo name is %s', self.url_repo_name)
368 369
369 370 ip_addr = get_ip_addr(environ)
370 371 user_agent = get_user_agent(environ)
371 372 username = None
372 373
373 374 # skip passing error to error controller
374 375 environ['pylons.status_code_redirect'] = True
375 376
376 377 # ======================================================================
377 378 # GET ACTION PULL or PUSH
378 379 # ======================================================================
379 380 action = self._get_action(environ)
380 381
381 382 # ======================================================================
382 383 # Check if this is a request to a shadow repository of a pull request.
383 384 # In this case only pull action is allowed.
384 385 # ======================================================================
385 386 if self.is_shadow_repo and action != 'pull':
386 387 reason = 'Only pull action is allowed for shadow repositories.'
387 388 log.debug('User not allowed to proceed, %s', reason)
388 389 return HTTPNotAcceptable(reason)(environ, start_response)
389 390
390 391 # Check if the shadow repo actually exists, in case someone refers
391 392 # to it, and it has been deleted because of successful merge.
392 393 if self.is_shadow_repo and not self.is_shadow_repo_dir:
393 394 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
394 395 self.is_shadow_repo_dir)
395 396 return HTTPNotFound()(environ, start_response)
396 397
397 398 # ======================================================================
398 399 # CHECK ANONYMOUS PERMISSION
399 400 # ======================================================================
400 401 if action in ['pull', 'push']:
401 402 anonymous_user = User.get_default_user()
402 403 username = anonymous_user.username
403 404 if anonymous_user.active:
404 405 # ONLY check permissions if the user is activated
405 406 anonymous_perm = self._check_permission(
406 407 action, anonymous_user, self.acl_repo_name, ip_addr)
407 408 else:
408 409 anonymous_perm = False
409 410
410 411 if not anonymous_user.active or not anonymous_perm:
411 412 if not anonymous_user.active:
412 413 log.debug('Anonymous access is disabled, running '
413 414 'authentication')
414 415
415 416 if not anonymous_perm:
416 417 log.debug('Not enough credentials to access this '
417 418 'repository as anonymous user')
418 419
419 420 username = None
420 421 # ==============================================================
421 422 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
422 423 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
423 424 # ==============================================================
424 425
425 426 # try to auth based on environ, container auth methods
426 427 log.debug('Running PRE-AUTH for container based authentication')
427 428 pre_auth = authenticate(
428 429 '', '', environ, VCS_TYPE, registry=self.registry,
429 430 acl_repo_name=self.acl_repo_name)
430 431 if pre_auth and pre_auth.get('username'):
431 432 username = pre_auth['username']
432 433 log.debug('PRE-AUTH got %s as username', username)
433 434 if pre_auth:
434 435 log.debug('PRE-AUTH successful from %s',
435 436 pre_auth.get('auth_data', {}).get('_plugin'))
436 437
437 438 # If not authenticated by the container, running basic auth
438 439 # before inject the calling repo_name for special scope checks
439 440 self.authenticate.acl_repo_name = self.acl_repo_name
440 441
441 442 plugin_cache_active, cache_ttl = False, 0
442 443 plugin = None
443 444 if not username:
444 445 self.authenticate.realm = self.authenticate.get_rc_realm()
445 446
446 447 try:
447 448 auth_result = self.authenticate(environ)
448 449 except (UserCreationError, NotAllowedToCreateUserError) as e:
449 450 log.error(e)
450 451 reason = safe_str(e)
451 452 return HTTPNotAcceptable(reason)(environ, start_response)
452 453
453 454 if isinstance(auth_result, dict):
454 455 AUTH_TYPE.update(environ, 'basic')
455 456 REMOTE_USER.update(environ, auth_result['username'])
456 457 username = auth_result['username']
457 458 plugin = auth_result.get('auth_data', {}).get('_plugin')
458 459 log.info(
459 460 'MAIN-AUTH successful for user `%s` from %s plugin',
460 461 username, plugin)
461 462
462 463 plugin_cache_active, cache_ttl = auth_result.get(
463 464 'auth_data', {}).get('_ttl_cache') or (False, 0)
464 465 else:
465 466 return auth_result.wsgi_application(
466 467 environ, start_response)
467 468
468 469
469 470 # ==============================================================
470 471 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
471 472 # ==============================================================
472 473 user = User.get_by_username(username)
473 474 if not self.valid_and_active_user(user):
474 475 return HTTPForbidden()(environ, start_response)
475 476 username = user.username
476 477 user.update_lastactivity()
477 478 meta.Session().commit()
478 479
479 480 # check user attributes for password change flag
480 481 user_obj = user
481 482 if user_obj and user_obj.username != User.DEFAULT_USER and \
482 483 user_obj.user_data.get('force_password_change'):
483 484 reason = 'password change required'
484 485 log.debug('User not allowed to authenticate, %s', reason)
485 486 return HTTPNotAcceptable(reason)(environ, start_response)
486 487
487 488 # check permissions for this repository
488 489 perm = self._check_permission(
489 490 action, user, self.acl_repo_name, ip_addr,
490 491 plugin, plugin_cache_active, cache_ttl)
491 492 if not perm:
492 493 return HTTPForbidden()(environ, start_response)
493 494
494 495 # extras are injected into UI object and later available
495 496 # in hooks executed by RhodeCode
496 497 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
497 498 extras = vcs_operation_context(
498 499 environ, repo_name=self.acl_repo_name, username=username,
499 500 action=action, scm=self.SCM, check_locking=check_locking,
500 501 is_shadow_repo=self.is_shadow_repo
501 502 )
502 503
503 504 # ======================================================================
504 505 # REQUEST HANDLING
505 506 # ======================================================================
506 507 repo_path = os.path.join(
507 508 safe_str(self.base_path), safe_str(self.vcs_repo_name))
508 509 log.debug('Repository path is %s', repo_path)
509 510
510 511 fix_PATH()
511 512
512 513 log.info(
513 514 '%s action on %s repo "%s" by "%s" from %s %s',
514 515 action, self.SCM, safe_str(self.url_repo_name),
515 516 safe_str(username), ip_addr, user_agent)
516 517
517 518 return self._generate_vcs_response(
518 519 environ, start_response, repo_path, extras, action)
519 520
520 521 @initialize_generator
521 522 def _generate_vcs_response(
522 523 self, environ, start_response, repo_path, extras, action):
523 524 """
524 525 Returns a generator for the response content.
525 526
526 527 This method is implemented as a generator, so that it can trigger
527 528 the cache validation after all content sent back to the client. It
528 529 also handles the locking exceptions which will be triggered when
529 530 the first chunk is produced by the underlying WSGI application.
530 531 """
531 532 callback_daemon, extras = self._prepare_callback_daemon(extras)
532 533 config = self._create_config(extras, self.acl_repo_name)
533 534 log.debug('HOOKS extras is %s', extras)
534 535 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
536 app.rc_extras = extras
535 537
536 538 try:
537 539 with callback_daemon:
538 540 try:
539 541 response = app(environ, start_response)
540 542 finally:
541 543 # This statement works together with the decorator
542 544 # "initialize_generator" above. The decorator ensures that
543 545 # we hit the first yield statement before the generator is
544 546 # returned back to the WSGI server. This is needed to
545 547 # ensure that the call to "app" above triggers the
546 548 # needed callback to "start_response" before the
547 549 # generator is actually used.
548 550 yield "__init__"
549 551
550 552 for chunk in response:
551 553 yield chunk
552 554 except Exception as exc:
553 555 # TODO: martinb: Exceptions are only raised in case of the Pyro4
554 556 # backend. Refactor this except block after dropping Pyro4 support.
555 557 # TODO: johbo: Improve "translating" back the exception.
556 558 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
557 559 exc = HTTPLockedRC(*exc.args)
558 560 _code = rhodecode.CONFIG.get('lock_ret_code')
559 561 log.debug('Repository LOCKED ret code %s!', (_code,))
560 562 elif getattr(exc, '_vcs_kind', None) == 'requirement':
561 563 log.debug(
562 564 'Repository requires features unknown to this Mercurial')
563 565 exc = HTTPRequirementError(*exc.args)
564 566 else:
565 567 raise
566 568
567 569 for chunk in exc(environ, start_response):
568 570 yield chunk
569 571 finally:
570 572 # invalidate cache on push
571 573 try:
572 574 if action == 'push':
573 575 self._invalidate_cache(self.url_repo_name)
574 576 finally:
575 577 meta.Session.remove()
576 578
577 579 def _get_repository_name(self, environ):
578 580 """Get repository name out of the environmnent
579 581
580 582 :param environ: WSGI environment
581 583 """
582 584 raise NotImplementedError()
583 585
584 586 def _get_action(self, environ):
585 587 """Map request commands into a pull or push command.
586 588
587 589 :param environ: WSGI environment
588 590 """
589 591 raise NotImplementedError()
590 592
591 593 def _create_wsgi_app(self, repo_path, repo_name, config):
592 594 """Return the WSGI app that will finally handle the request."""
593 595 raise NotImplementedError()
594 596
595 597 def _create_config(self, extras, repo_name):
596 598 """Create a safe config representation."""
597 599 raise NotImplementedError()
598 600
599 601 def _prepare_callback_daemon(self, extras):
600 602 return prepare_callback_daemon(
601 603 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
602 604 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
603 605
604 606
605 607 def _should_check_locking(query_string):
606 608 # this is kind of hacky, but due to how mercurial handles client-server
607 609 # server see all operation on commit; bookmarks, phases and
608 610 # obsolescence marker in different transaction, we don't want to check
609 611 # locking on those
610 612 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now