##// END OF EJS Templates
vcs: do an early detection of vcs-type request....
marcink -
r1297:de699d5e default
parent child Browse files
Show More
@@ -1,529 +1,529 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import importlib
28 import importlib
29 import re
29 import re
30 from functools import wraps
30 from functools import wraps
31
31
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from webob.exc import (
33 from webob.exc import (
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
40 from rhodecode.lib.exceptions import (
40 from rhodecode.lib.exceptions import (
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 NotAllowedToCreateUserError)
42 NotAllowedToCreateUserError)
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.middleware import appenlight
44 from rhodecode.lib.middleware import appenlight
45 from rhodecode.lib.middleware.utils import scm_app, scm_app_http
45 from rhodecode.lib.middleware.utils import scm_app, scm_app_http
46 from rhodecode.lib.utils import (
46 from rhodecode.lib.utils import (
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 from rhodecode.lib.vcs.backends import base
50 from rhodecode.lib.vcs.backends import base
51 from rhodecode.model import meta
51 from rhodecode.model import meta
52 from rhodecode.model.db import User, Repository, PullRequest
52 from rhodecode.model.db import User, Repository, PullRequest
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.pull_request import PullRequestModel
54 from rhodecode.model.pull_request import PullRequestModel
55
55
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 def initialize_generator(factory):
60 def initialize_generator(factory):
61 """
61 """
62 Initializes the returned generator by draining its first element.
62 Initializes the returned generator by draining its first element.
63
63
64 This can be used to give a generator an initializer, which is the code
64 This can be used to give a generator an initializer, which is the code
65 up to the first yield statement. This decorator enforces that the first
65 up to the first yield statement. This decorator enforces that the first
66 produced element has the value ``"__init__"`` to make its special
66 produced element has the value ``"__init__"`` to make its special
67 purpose very explicit in the using code.
67 purpose very explicit in the using code.
68 """
68 """
69
69
70 @wraps(factory)
70 @wraps(factory)
71 def wrapper(*args, **kwargs):
71 def wrapper(*args, **kwargs):
72 gen = factory(*args, **kwargs)
72 gen = factory(*args, **kwargs)
73 try:
73 try:
74 init = gen.next()
74 init = gen.next()
75 except StopIteration:
75 except StopIteration:
76 raise ValueError('Generator must yield at least one element.')
76 raise ValueError('Generator must yield at least one element.')
77 if init != "__init__":
77 if init != "__init__":
78 raise ValueError('First yielded element must be "__init__".')
78 raise ValueError('First yielded element must be "__init__".')
79 return gen
79 return gen
80 return wrapper
80 return wrapper
81
81
82
82
83 class SimpleVCS(object):
83 class SimpleVCS(object):
84 """Common functionality for SCM HTTP handlers."""
84 """Common functionality for SCM HTTP handlers."""
85
85
86 SCM = 'unknown'
86 SCM = 'unknown'
87
87
88 acl_repo_name = None
88 acl_repo_name = None
89 url_repo_name = None
89 url_repo_name = None
90 vcs_repo_name = None
90 vcs_repo_name = None
91
91
92 # We have to handle requests to shadow repositories different than requests
92 # We have to handle requests to shadow repositories different than requests
93 # to normal repositories. Therefore we have to distinguish them. To do this
93 # to normal repositories. Therefore we have to distinguish them. To do this
94 # we use this regex which will match only on URLs pointing to shadow
94 # we use this regex which will match only on URLs pointing to shadow
95 # repositories.
95 # repositories.
96 shadow_repo_re = re.compile(
96 shadow_repo_re = re.compile(
97 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
97 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
98 '(?P<target>{slug_pat})/' # target repo
98 '(?P<target>{slug_pat})/' # target repo
99 'pull-request/(?P<pr_id>\d+)/' # pull request
99 'pull-request/(?P<pr_id>\d+)/' # pull request
100 'repository$' # shadow repo
100 'repository$' # shadow repo
101 .format(slug_pat=SLUG_RE.pattern))
101 .format(slug_pat=SLUG_RE.pattern))
102
102
103 def __init__(self, application, config, registry):
103 def __init__(self, application, config, registry):
104 self.registry = registry
104 self.registry = registry
105 self.application = application
105 self.application = application
106 self.config = config
106 self.config = config
107 # re-populated by specialized middleware
107 # re-populated by specialized middleware
108 self.repo_vcs_config = base.Config()
108 self.repo_vcs_config = base.Config()
109
109
110 # base path of repo locations
110 # base path of repo locations
111 self.basepath = get_rhodecode_base_path()
111 self.basepath = get_rhodecode_base_path()
112 # authenticate this VCS request using authfunc
112 # authenticate this VCS request using authfunc
113 auth_ret_code_detection = \
113 auth_ret_code_detection = \
114 str2bool(self.config.get('auth_ret_code_detection', False))
114 str2bool(self.config.get('auth_ret_code_detection', False))
115 self.authenticate = BasicAuth(
115 self.authenticate = BasicAuth(
116 '', authenticate, registry, config.get('auth_ret_code'),
116 '', authenticate, registry, config.get('auth_ret_code'),
117 auth_ret_code_detection)
117 auth_ret_code_detection)
118 self.ip_addr = '0.0.0.0'
118 self.ip_addr = '0.0.0.0'
119
119
120 def set_repo_names(self, environ):
120 def set_repo_names(self, environ):
121 """
121 """
122 This will populate the attributes acl_repo_name, url_repo_name,
122 This will populate the attributes acl_repo_name, url_repo_name,
123 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
123 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
124 shadow) repositories all names are equal. In case of requests to a
124 shadow) repositories all names are equal. In case of requests to a
125 shadow repository the acl-name points to the target repo of the pull
125 shadow repository the acl-name points to the target repo of the pull
126 request and the vcs-name points to the shadow repo file system path.
126 request and the vcs-name points to the shadow repo file system path.
127 The url-name is always the URL used by the vcs client program.
127 The url-name is always the URL used by the vcs client program.
128
128
129 Example in case of a shadow repo:
129 Example in case of a shadow repo:
130 acl_repo_name = RepoGroup/MyRepo
130 acl_repo_name = RepoGroup/MyRepo
131 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
131 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
132 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
132 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
133 """
133 """
134 # First we set the repo name from URL for all attributes. This is the
134 # First we set the repo name from URL for all attributes. This is the
135 # default if handling normal (non shadow) repo requests.
135 # default if handling normal (non shadow) repo requests.
136 self.url_repo_name = self._get_repository_name(environ)
136 self.url_repo_name = self._get_repository_name(environ)
137 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
137 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
138 self.is_shadow_repo = False
138 self.is_shadow_repo = False
139
139
140 # Check if this is a request to a shadow repository.
140 # Check if this is a request to a shadow repository.
141 match = self.shadow_repo_re.match(self.url_repo_name)
141 match = self.shadow_repo_re.match(self.url_repo_name)
142 if match:
142 if match:
143 match_dict = match.groupdict()
143 match_dict = match.groupdict()
144
144
145 # Build acl repo name from regex match.
145 # Build acl repo name from regex match.
146 acl_repo_name = safe_unicode('{groups}{target}'.format(
146 acl_repo_name = safe_unicode('{groups}{target}'.format(
147 groups=match_dict['groups'] or '',
147 groups=match_dict['groups'] or '',
148 target=match_dict['target']))
148 target=match_dict['target']))
149
149
150 # Retrieve pull request instance by ID from regex match.
150 # Retrieve pull request instance by ID from regex match.
151 pull_request = PullRequest.get(match_dict['pr_id'])
151 pull_request = PullRequest.get(match_dict['pr_id'])
152
152
153 # Only proceed if we got a pull request and if acl repo name from
153 # Only proceed if we got a pull request and if acl repo name from
154 # URL equals the target repo name of the pull request.
154 # URL equals the target repo name of the pull request.
155 if pull_request and (acl_repo_name ==
155 if pull_request and (acl_repo_name ==
156 pull_request.target_repo.repo_name):
156 pull_request.target_repo.repo_name):
157 # Get file system path to shadow repository.
157 # Get file system path to shadow repository.
158 workspace_id = PullRequestModel()._workspace_id(pull_request)
158 workspace_id = PullRequestModel()._workspace_id(pull_request)
159 target_vcs = pull_request.target_repo.scm_instance()
159 target_vcs = pull_request.target_repo.scm_instance()
160 vcs_repo_name = target_vcs._get_shadow_repository_path(
160 vcs_repo_name = target_vcs._get_shadow_repository_path(
161 workspace_id)
161 workspace_id)
162
162
163 # Store names for later usage.
163 # Store names for later usage.
164 self.vcs_repo_name = vcs_repo_name
164 self.vcs_repo_name = vcs_repo_name
165 self.acl_repo_name = acl_repo_name
165 self.acl_repo_name = acl_repo_name
166 self.is_shadow_repo = True
166 self.is_shadow_repo = True
167
167
168 log.debug('Repository names: %s', {
168 log.debug('Setting all VCS repository names: %s', {
169 'acl_repo_name': self.acl_repo_name,
169 'acl_repo_name': self.acl_repo_name,
170 'url_repo_name': self.url_repo_name,
170 'url_repo_name': self.url_repo_name,
171 'vcs_repo_name': self.vcs_repo_name,
171 'vcs_repo_name': self.vcs_repo_name,
172 })
172 })
173
173
174 @property
174 @property
175 def scm_app(self):
175 def scm_app(self):
176 custom_implementation = self.config['vcs.scm_app_implementation']
176 custom_implementation = self.config['vcs.scm_app_implementation']
177 if custom_implementation == 'http':
177 if custom_implementation == 'http':
178 log.info('Using HTTP implementation of scm app.')
178 log.info('Using HTTP implementation of scm app.')
179 scm_app_impl = scm_app_http
179 scm_app_impl = scm_app_http
180 elif custom_implementation == 'pyro4':
180 elif custom_implementation == 'pyro4':
181 log.info('Using Pyro implementation of scm app.')
181 log.info('Using Pyro implementation of scm app.')
182 scm_app_impl = scm_app
182 scm_app_impl = scm_app
183 else:
183 else:
184 log.info('Using custom implementation of scm_app: "{}"'.format(
184 log.info('Using custom implementation of scm_app: "{}"'.format(
185 custom_implementation))
185 custom_implementation))
186 scm_app_impl = importlib.import_module(custom_implementation)
186 scm_app_impl = importlib.import_module(custom_implementation)
187 return scm_app_impl
187 return scm_app_impl
188
188
189 def _get_by_id(self, repo_name):
189 def _get_by_id(self, repo_name):
190 """
190 """
191 Gets a special pattern _<ID> from clone url and tries to replace it
191 Gets a special pattern _<ID> from clone url and tries to replace it
192 with a repository_name for support of _<ID> non changeable urls
192 with a repository_name for support of _<ID> non changeable urls
193 """
193 """
194
194
195 data = repo_name.split('/')
195 data = repo_name.split('/')
196 if len(data) >= 2:
196 if len(data) >= 2:
197 from rhodecode.model.repo import RepoModel
197 from rhodecode.model.repo import RepoModel
198 by_id_match = RepoModel().get_repo_by_id(repo_name)
198 by_id_match = RepoModel().get_repo_by_id(repo_name)
199 if by_id_match:
199 if by_id_match:
200 data[1] = by_id_match.repo_name
200 data[1] = by_id_match.repo_name
201
201
202 return safe_str('/'.join(data))
202 return safe_str('/'.join(data))
203
203
204 def _invalidate_cache(self, repo_name):
204 def _invalidate_cache(self, repo_name):
205 """
205 """
206 Set's cache for this repository for invalidation on next access
206 Set's cache for this repository for invalidation on next access
207
207
208 :param repo_name: full repo name, also a cache key
208 :param repo_name: full repo name, also a cache key
209 """
209 """
210 ScmModel().mark_for_invalidation(repo_name)
210 ScmModel().mark_for_invalidation(repo_name)
211
211
212 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
212 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
213 db_repo = Repository.get_by_repo_name(repo_name)
213 db_repo = Repository.get_by_repo_name(repo_name)
214 if not db_repo:
214 if not db_repo:
215 log.debug('Repository `%s` not found inside the database.',
215 log.debug('Repository `%s` not found inside the database.',
216 repo_name)
216 repo_name)
217 return False
217 return False
218
218
219 if db_repo.repo_type != scm_type:
219 if db_repo.repo_type != scm_type:
220 log.warning(
220 log.warning(
221 'Repository `%s` have incorrect scm_type, expected %s got %s',
221 'Repository `%s` have incorrect scm_type, expected %s got %s',
222 repo_name, db_repo.repo_type, scm_type)
222 repo_name, db_repo.repo_type, scm_type)
223 return False
223 return False
224
224
225 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
225 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
226
226
227 def valid_and_active_user(self, user):
227 def valid_and_active_user(self, user):
228 """
228 """
229 Checks if that user is not empty, and if it's actually object it checks
229 Checks if that user is not empty, and if it's actually object it checks
230 if he's active.
230 if he's active.
231
231
232 :param user: user object or None
232 :param user: user object or None
233 :return: boolean
233 :return: boolean
234 """
234 """
235 if user is None:
235 if user is None:
236 return False
236 return False
237
237
238 elif user.active:
238 elif user.active:
239 return True
239 return True
240
240
241 return False
241 return False
242
242
243 def _check_permission(self, action, user, repo_name, ip_addr=None):
243 def _check_permission(self, action, user, repo_name, ip_addr=None):
244 """
244 """
245 Checks permissions using action (push/pull) user and repository
245 Checks permissions using action (push/pull) user and repository
246 name
246 name
247
247
248 :param action: push or pull action
248 :param action: push or pull action
249 :param user: user instance
249 :param user: user instance
250 :param repo_name: repository name
250 :param repo_name: repository name
251 """
251 """
252 # check IP
252 # check IP
253 inherit = user.inherit_default_permissions
253 inherit = user.inherit_default_permissions
254 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
254 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
255 inherit_from_default=inherit)
255 inherit_from_default=inherit)
256 if ip_allowed:
256 if ip_allowed:
257 log.info('Access for IP:%s allowed', ip_addr)
257 log.info('Access for IP:%s allowed', ip_addr)
258 else:
258 else:
259 return False
259 return False
260
260
261 if action == 'push':
261 if action == 'push':
262 if not HasPermissionAnyMiddleware('repository.write',
262 if not HasPermissionAnyMiddleware('repository.write',
263 'repository.admin')(user,
263 'repository.admin')(user,
264 repo_name):
264 repo_name):
265 return False
265 return False
266
266
267 else:
267 else:
268 # any other action need at least read permission
268 # any other action need at least read permission
269 if not HasPermissionAnyMiddleware('repository.read',
269 if not HasPermissionAnyMiddleware('repository.read',
270 'repository.write',
270 'repository.write',
271 'repository.admin')(user,
271 'repository.admin')(user,
272 repo_name):
272 repo_name):
273 return False
273 return False
274
274
275 return True
275 return True
276
276
277 def _check_ssl(self, environ, start_response):
277 def _check_ssl(self, environ, start_response):
278 """
278 """
279 Checks the SSL check flag and returns False if SSL is not present
279 Checks the SSL check flag and returns False if SSL is not present
280 and required True otherwise
280 and required True otherwise
281 """
281 """
282 org_proto = environ['wsgi._org_proto']
282 org_proto = environ['wsgi._org_proto']
283 # check if we have SSL required ! if not it's a bad request !
283 # check if we have SSL required ! if not it's a bad request !
284 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
284 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
285 if require_ssl and org_proto == 'http':
285 if require_ssl and org_proto == 'http':
286 log.debug('proto is %s and SSL is required BAD REQUEST !',
286 log.debug('proto is %s and SSL is required BAD REQUEST !',
287 org_proto)
287 org_proto)
288 return False
288 return False
289 return True
289 return True
290
290
291 def __call__(self, environ, start_response):
291 def __call__(self, environ, start_response):
292 try:
292 try:
293 return self._handle_request(environ, start_response)
293 return self._handle_request(environ, start_response)
294 except Exception:
294 except Exception:
295 log.exception("Exception while handling request")
295 log.exception("Exception while handling request")
296 appenlight.track_exception(environ)
296 appenlight.track_exception(environ)
297 return HTTPInternalServerError()(environ, start_response)
297 return HTTPInternalServerError()(environ, start_response)
298 finally:
298 finally:
299 meta.Session.remove()
299 meta.Session.remove()
300
300
301 def _handle_request(self, environ, start_response):
301 def _handle_request(self, environ, start_response):
302
302
303 if not self._check_ssl(environ, start_response):
303 if not self._check_ssl(environ, start_response):
304 reason = ('SSL required, while RhodeCode was unable '
304 reason = ('SSL required, while RhodeCode was unable '
305 'to detect this as SSL request')
305 'to detect this as SSL request')
306 log.debug('User not allowed to proceed, %s', reason)
306 log.debug('User not allowed to proceed, %s', reason)
307 return HTTPNotAcceptable(reason)(environ, start_response)
307 return HTTPNotAcceptable(reason)(environ, start_response)
308
308
309 if not self.url_repo_name:
309 if not self.url_repo_name:
310 log.warning('Repository name is empty: %s', self.url_repo_name)
310 log.warning('Repository name is empty: %s', self.url_repo_name)
311 # failed to get repo name, we fail now
311 # failed to get repo name, we fail now
312 return HTTPNotFound()(environ, start_response)
312 return HTTPNotFound()(environ, start_response)
313 log.debug('Extracted repo name is %s', self.url_repo_name)
313 log.debug('Extracted repo name is %s', self.url_repo_name)
314
314
315 ip_addr = get_ip_addr(environ)
315 ip_addr = get_ip_addr(environ)
316 username = None
316 username = None
317
317
318 # skip passing error to error controller
318 # skip passing error to error controller
319 environ['pylons.status_code_redirect'] = True
319 environ['pylons.status_code_redirect'] = True
320
320
321 # ======================================================================
321 # ======================================================================
322 # GET ACTION PULL or PUSH
322 # GET ACTION PULL or PUSH
323 # ======================================================================
323 # ======================================================================
324 action = self._get_action(environ)
324 action = self._get_action(environ)
325
325
326 # ======================================================================
326 # ======================================================================
327 # Check if this is a request to a shadow repository of a pull request.
327 # Check if this is a request to a shadow repository of a pull request.
328 # In this case only pull action is allowed.
328 # In this case only pull action is allowed.
329 # ======================================================================
329 # ======================================================================
330 if self.is_shadow_repo and action != 'pull':
330 if self.is_shadow_repo and action != 'pull':
331 reason = 'Only pull action is allowed for shadow repositories.'
331 reason = 'Only pull action is allowed for shadow repositories.'
332 log.debug('User not allowed to proceed, %s', reason)
332 log.debug('User not allowed to proceed, %s', reason)
333 return HTTPNotAcceptable(reason)(environ, start_response)
333 return HTTPNotAcceptable(reason)(environ, start_response)
334
334
335 # ======================================================================
335 # ======================================================================
336 # CHECK ANONYMOUS PERMISSION
336 # CHECK ANONYMOUS PERMISSION
337 # ======================================================================
337 # ======================================================================
338 if action in ['pull', 'push']:
338 if action in ['pull', 'push']:
339 anonymous_user = User.get_default_user()
339 anonymous_user = User.get_default_user()
340 username = anonymous_user.username
340 username = anonymous_user.username
341 if anonymous_user.active:
341 if anonymous_user.active:
342 # ONLY check permissions if the user is activated
342 # ONLY check permissions if the user is activated
343 anonymous_perm = self._check_permission(
343 anonymous_perm = self._check_permission(
344 action, anonymous_user, self.acl_repo_name, ip_addr)
344 action, anonymous_user, self.acl_repo_name, ip_addr)
345 else:
345 else:
346 anonymous_perm = False
346 anonymous_perm = False
347
347
348 if not anonymous_user.active or not anonymous_perm:
348 if not anonymous_user.active or not anonymous_perm:
349 if not anonymous_user.active:
349 if not anonymous_user.active:
350 log.debug('Anonymous access is disabled, running '
350 log.debug('Anonymous access is disabled, running '
351 'authentication')
351 'authentication')
352
352
353 if not anonymous_perm:
353 if not anonymous_perm:
354 log.debug('Not enough credentials to access this '
354 log.debug('Not enough credentials to access this '
355 'repository as anonymous user')
355 'repository as anonymous user')
356
356
357 username = None
357 username = None
358 # ==============================================================
358 # ==============================================================
359 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
359 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
360 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
360 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
361 # ==============================================================
361 # ==============================================================
362
362
363 # try to auth based on environ, container auth methods
363 # try to auth based on environ, container auth methods
364 log.debug('Running PRE-AUTH for container based authentication')
364 log.debug('Running PRE-AUTH for container based authentication')
365 pre_auth = authenticate(
365 pre_auth = authenticate(
366 '', '', environ, VCS_TYPE, registry=self.registry)
366 '', '', environ, VCS_TYPE, registry=self.registry)
367 if pre_auth and pre_auth.get('username'):
367 if pre_auth and pre_auth.get('username'):
368 username = pre_auth['username']
368 username = pre_auth['username']
369 log.debug('PRE-AUTH got %s as username', username)
369 log.debug('PRE-AUTH got %s as username', username)
370
370
371 # If not authenticated by the container, running basic auth
371 # If not authenticated by the container, running basic auth
372 if not username:
372 if not username:
373 self.authenticate.realm = get_rhodecode_realm()
373 self.authenticate.realm = get_rhodecode_realm()
374
374
375 try:
375 try:
376 result = self.authenticate(environ)
376 result = self.authenticate(environ)
377 except (UserCreationError, NotAllowedToCreateUserError) as e:
377 except (UserCreationError, NotAllowedToCreateUserError) as e:
378 log.error(e)
378 log.error(e)
379 reason = safe_str(e)
379 reason = safe_str(e)
380 return HTTPNotAcceptable(reason)(environ, start_response)
380 return HTTPNotAcceptable(reason)(environ, start_response)
381
381
382 if isinstance(result, str):
382 if isinstance(result, str):
383 AUTH_TYPE.update(environ, 'basic')
383 AUTH_TYPE.update(environ, 'basic')
384 REMOTE_USER.update(environ, result)
384 REMOTE_USER.update(environ, result)
385 username = result
385 username = result
386 else:
386 else:
387 return result.wsgi_application(environ, start_response)
387 return result.wsgi_application(environ, start_response)
388
388
389 # ==============================================================
389 # ==============================================================
390 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
390 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
391 # ==============================================================
391 # ==============================================================
392 user = User.get_by_username(username)
392 user = User.get_by_username(username)
393 if not self.valid_and_active_user(user):
393 if not self.valid_and_active_user(user):
394 return HTTPForbidden()(environ, start_response)
394 return HTTPForbidden()(environ, start_response)
395 username = user.username
395 username = user.username
396 user.update_lastactivity()
396 user.update_lastactivity()
397 meta.Session().commit()
397 meta.Session().commit()
398
398
399 # check user attributes for password change flag
399 # check user attributes for password change flag
400 user_obj = user
400 user_obj = user
401 if user_obj and user_obj.username != User.DEFAULT_USER and \
401 if user_obj and user_obj.username != User.DEFAULT_USER and \
402 user_obj.user_data.get('force_password_change'):
402 user_obj.user_data.get('force_password_change'):
403 reason = 'password change required'
403 reason = 'password change required'
404 log.debug('User not allowed to authenticate, %s', reason)
404 log.debug('User not allowed to authenticate, %s', reason)
405 return HTTPNotAcceptable(reason)(environ, start_response)
405 return HTTPNotAcceptable(reason)(environ, start_response)
406
406
407 # check permissions for this repository
407 # check permissions for this repository
408 perm = self._check_permission(
408 perm = self._check_permission(
409 action, user, self.acl_repo_name, ip_addr)
409 action, user, self.acl_repo_name, ip_addr)
410 if not perm:
410 if not perm:
411 return HTTPForbidden()(environ, start_response)
411 return HTTPForbidden()(environ, start_response)
412
412
413 # extras are injected into UI object and later available
413 # extras are injected into UI object and later available
414 # in hooks executed by rhodecode
414 # in hooks executed by rhodecode
415 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
415 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
416 extras = vcs_operation_context(
416 extras = vcs_operation_context(
417 environ, repo_name=self.acl_repo_name, username=username,
417 environ, repo_name=self.acl_repo_name, username=username,
418 action=action, scm=self.SCM, check_locking=check_locking,
418 action=action, scm=self.SCM, check_locking=check_locking,
419 is_shadow_repo=self.is_shadow_repo
419 is_shadow_repo=self.is_shadow_repo
420 )
420 )
421
421
422 # ======================================================================
422 # ======================================================================
423 # REQUEST HANDLING
423 # REQUEST HANDLING
424 # ======================================================================
424 # ======================================================================
425 repo_path = os.path.join(
425 repo_path = os.path.join(
426 safe_str(self.basepath), safe_str(self.vcs_repo_name))
426 safe_str(self.basepath), safe_str(self.vcs_repo_name))
427 log.debug('Repository path is %s', repo_path)
427 log.debug('Repository path is %s', repo_path)
428
428
429 fix_PATH()
429 fix_PATH()
430
430
431 log.info(
431 log.info(
432 '%s action on %s repo "%s" by "%s" from %s',
432 '%s action on %s repo "%s" by "%s" from %s',
433 action, self.SCM, safe_str(self.url_repo_name),
433 action, self.SCM, safe_str(self.url_repo_name),
434 safe_str(username), ip_addr)
434 safe_str(username), ip_addr)
435
435
436 return self._generate_vcs_response(
436 return self._generate_vcs_response(
437 environ, start_response, repo_path, extras, action)
437 environ, start_response, repo_path, extras, action)
438
438
439 @initialize_generator
439 @initialize_generator
440 def _generate_vcs_response(
440 def _generate_vcs_response(
441 self, environ, start_response, repo_path, extras, action):
441 self, environ, start_response, repo_path, extras, action):
442 """
442 """
443 Returns a generator for the response content.
443 Returns a generator for the response content.
444
444
445 This method is implemented as a generator, so that it can trigger
445 This method is implemented as a generator, so that it can trigger
446 the cache validation after all content sent back to the client. It
446 the cache validation after all content sent back to the client. It
447 also handles the locking exceptions which will be triggered when
447 also handles the locking exceptions which will be triggered when
448 the first chunk is produced by the underlying WSGI application.
448 the first chunk is produced by the underlying WSGI application.
449 """
449 """
450 callback_daemon, extras = self._prepare_callback_daemon(extras)
450 callback_daemon, extras = self._prepare_callback_daemon(extras)
451 config = self._create_config(extras, self.acl_repo_name)
451 config = self._create_config(extras, self.acl_repo_name)
452 log.debug('HOOKS extras is %s', extras)
452 log.debug('HOOKS extras is %s', extras)
453 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
453 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
454
454
455 try:
455 try:
456 with callback_daemon:
456 with callback_daemon:
457 try:
457 try:
458 response = app(environ, start_response)
458 response = app(environ, start_response)
459 finally:
459 finally:
460 # This statement works together with the decorator
460 # This statement works together with the decorator
461 # "initialize_generator" above. The decorator ensures that
461 # "initialize_generator" above. The decorator ensures that
462 # we hit the first yield statement before the generator is
462 # we hit the first yield statement before the generator is
463 # returned back to the WSGI server. This is needed to
463 # returned back to the WSGI server. This is needed to
464 # ensure that the call to "app" above triggers the
464 # ensure that the call to "app" above triggers the
465 # needed callback to "start_response" before the
465 # needed callback to "start_response" before the
466 # generator is actually used.
466 # generator is actually used.
467 yield "__init__"
467 yield "__init__"
468
468
469 for chunk in response:
469 for chunk in response:
470 yield chunk
470 yield chunk
471 except Exception as exc:
471 except Exception as exc:
472 # TODO: martinb: Exceptions are only raised in case of the Pyro4
472 # TODO: martinb: Exceptions are only raised in case of the Pyro4
473 # backend. Refactor this except block after dropping Pyro4 support.
473 # backend. Refactor this except block after dropping Pyro4 support.
474 # TODO: johbo: Improve "translating" back the exception.
474 # TODO: johbo: Improve "translating" back the exception.
475 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
475 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
476 exc = HTTPLockedRC(*exc.args)
476 exc = HTTPLockedRC(*exc.args)
477 _code = rhodecode.CONFIG.get('lock_ret_code')
477 _code = rhodecode.CONFIG.get('lock_ret_code')
478 log.debug('Repository LOCKED ret code %s!', (_code,))
478 log.debug('Repository LOCKED ret code %s!', (_code,))
479 elif getattr(exc, '_vcs_kind', None) == 'requirement':
479 elif getattr(exc, '_vcs_kind', None) == 'requirement':
480 log.debug(
480 log.debug(
481 'Repository requires features unknown to this Mercurial')
481 'Repository requires features unknown to this Mercurial')
482 exc = HTTPRequirementError(*exc.args)
482 exc = HTTPRequirementError(*exc.args)
483 else:
483 else:
484 raise
484 raise
485
485
486 for chunk in exc(environ, start_response):
486 for chunk in exc(environ, start_response):
487 yield chunk
487 yield chunk
488 finally:
488 finally:
489 # invalidate cache on push
489 # invalidate cache on push
490 try:
490 try:
491 if action == 'push':
491 if action == 'push':
492 self._invalidate_cache(self.url_repo_name)
492 self._invalidate_cache(self.url_repo_name)
493 finally:
493 finally:
494 meta.Session.remove()
494 meta.Session.remove()
495
495
496 def _get_repository_name(self, environ):
496 def _get_repository_name(self, environ):
497 """Get repository name out of the environmnent
497 """Get repository name out of the environmnent
498
498
499 :param environ: WSGI environment
499 :param environ: WSGI environment
500 """
500 """
501 raise NotImplementedError()
501 raise NotImplementedError()
502
502
503 def _get_action(self, environ):
503 def _get_action(self, environ):
504 """Map request commands into a pull or push command.
504 """Map request commands into a pull or push command.
505
505
506 :param environ: WSGI environment
506 :param environ: WSGI environment
507 """
507 """
508 raise NotImplementedError()
508 raise NotImplementedError()
509
509
510 def _create_wsgi_app(self, repo_path, repo_name, config):
510 def _create_wsgi_app(self, repo_path, repo_name, config):
511 """Return the WSGI app that will finally handle the request."""
511 """Return the WSGI app that will finally handle the request."""
512 raise NotImplementedError()
512 raise NotImplementedError()
513
513
514 def _create_config(self, extras, repo_name):
514 def _create_config(self, extras, repo_name):
515 """Create a Pyro safe config representation."""
515 """Create a Pyro safe config representation."""
516 raise NotImplementedError()
516 raise NotImplementedError()
517
517
518 def _prepare_callback_daemon(self, extras):
518 def _prepare_callback_daemon(self, extras):
519 return prepare_callback_daemon(
519 return prepare_callback_daemon(
520 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
520 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
521 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
521 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
522
522
523
523
524 def _should_check_locking(query_string):
524 def _should_check_locking(query_string):
525 # this is kind of hacky, but due to how mercurial handles client-server
525 # this is kind of hacky, but due to how mercurial handles client-server
526 # server see all operation on commit; bookmarks, phases and
526 # server see all operation on commit; bookmarks, phases and
527 # obsolescence marker in different transaction, we don't want to check
527 # obsolescence marker in different transaction, we don't want to check
528 # locking on those
528 # locking on those
529 return query_string not in ['cmd=listkeys']
529 return query_string not in ['cmd=listkeys']
@@ -1,204 +1,235 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import gzip
21 import gzip
22 import shutil
22 import shutil
23 import logging
23 import logging
24 import tempfile
24 import tempfile
25 import urlparse
25 import urlparse
26
26
27 from webob.exc import HTTPNotFound
27 from webob.exc import HTTPNotFound
28
28
29 import rhodecode
29 import rhodecode
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 from rhodecode.lib.middleware.simplehg import SimpleHg
32 from rhodecode.lib.middleware.simplehg import SimpleHg
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
34 from rhodecode.model.settings import VcsSettingsModel
34 from rhodecode.model.settings import VcsSettingsModel
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38 VCS_TYPE_KEY = '_rc_vcs_type'
39 VCS_TYPE_SKIP = '_rc_vcs_skip'
40
38
41
39 def is_git(environ):
42 def is_git(environ):
40 """
43 """
41 Returns True if requests should be handled by GIT wsgi middleware
44 Returns True if requests should be handled by GIT wsgi middleware
42 """
45 """
43 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
46 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
44 log.debug(
47 log.debug(
45 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
48 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
46 is_git_path is not None)
49 is_git_path is not None)
47
50
48 return is_git_path
51 return is_git_path
49
52
50
53
51 def is_hg(environ):
54 def is_hg(environ):
52 """
55 """
53 Returns True if requests target is mercurial server - header
56 Returns True if requests target is mercurial server - header
54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
57 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
55 """
58 """
56 is_hg_path = False
59 is_hg_path = False
57
60
58 http_accept = environ.get('HTTP_ACCEPT')
61 http_accept = environ.get('HTTP_ACCEPT')
59
62
60 if http_accept and http_accept.startswith('application/mercurial'):
63 if http_accept and http_accept.startswith('application/mercurial'):
61 query = urlparse.parse_qs(environ['QUERY_STRING'])
64 query = urlparse.parse_qs(environ['QUERY_STRING'])
62 if 'cmd' in query:
65 if 'cmd' in query:
63 is_hg_path = True
66 is_hg_path = True
64
67
65 log.debug(
68 log.debug(
66 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
69 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
67 is_hg_path)
70 is_hg_path)
68
71
69 return is_hg_path
72 return is_hg_path
70
73
71
74
72 def is_svn(environ):
75 def is_svn(environ):
73 """
76 """
74 Returns True if requests target is Subversion server
77 Returns True if requests target is Subversion server
75 """
78 """
76 http_dav = environ.get('HTTP_DAV', '')
79 http_dav = environ.get('HTTP_DAV', '')
77 magic_path_segment = rhodecode.CONFIG.get(
80 magic_path_segment = rhodecode.CONFIG.get(
78 'rhodecode_subversion_magic_path', '/!svn')
81 'rhodecode_subversion_magic_path', '/!svn')
79 is_svn_path = (
82 is_svn_path = (
80 'subversion' in http_dav or
83 'subversion' in http_dav or
81 magic_path_segment in environ['PATH_INFO'])
84 magic_path_segment in environ['PATH_INFO'])
82 log.debug(
85 log.debug(
83 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
86 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
84 is_svn_path)
87 is_svn_path)
85
88
86 return is_svn_path
89 return is_svn_path
87
90
88
91
89 class GunzipMiddleware(object):
92 class GunzipMiddleware(object):
90 """
93 """
91 WSGI middleware that unzips gzip-encoded requests before
94 WSGI middleware that unzips gzip-encoded requests before
92 passing on to the underlying application.
95 passing on to the underlying application.
93 """
96 """
94
97
95 def __init__(self, application):
98 def __init__(self, application):
96 self.app = application
99 self.app = application
97
100
98 def __call__(self, environ, start_response):
101 def __call__(self, environ, start_response):
99 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
102 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
100
103
101 if b'gzip' in accepts_encoding_header:
104 if b'gzip' in accepts_encoding_header:
102 log.debug('gzip detected, now running gunzip wrapper')
105 log.debug('gzip detected, now running gunzip wrapper')
103 wsgi_input = environ['wsgi.input']
106 wsgi_input = environ['wsgi.input']
104
107
105 if not hasattr(environ['wsgi.input'], 'seek'):
108 if not hasattr(environ['wsgi.input'], 'seek'):
106 # The gzip implementation in the standard library of Python 2.x
109 # The gzip implementation in the standard library of Python 2.x
107 # requires the '.seek()' and '.tell()' methods to be available
110 # requires the '.seek()' and '.tell()' methods to be available
108 # on the input stream. Read the data into a temporary file to
111 # on the input stream. Read the data into a temporary file to
109 # work around this limitation.
112 # work around this limitation.
110
113
111 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
114 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
112 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
115 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
113 wsgi_input.seek(0)
116 wsgi_input.seek(0)
114
117
115 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
118 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
116 # since we "Ungzipped" the content we say now it's no longer gzip
119 # since we "Ungzipped" the content we say now it's no longer gzip
117 # content encoding
120 # content encoding
118 del environ['HTTP_CONTENT_ENCODING']
121 del environ['HTTP_CONTENT_ENCODING']
119
122
120 # content length has changes ? or i'm not sure
123 # content length has changes ? or i'm not sure
121 if 'CONTENT_LENGTH' in environ:
124 if 'CONTENT_LENGTH' in environ:
122 del environ['CONTENT_LENGTH']
125 del environ['CONTENT_LENGTH']
123 else:
126 else:
124 log.debug('content not gzipped, gzipMiddleware passing '
127 log.debug('content not gzipped, gzipMiddleware passing '
125 'request further')
128 'request further')
126 return self.app(environ, start_response)
129 return self.app(environ, start_response)
127
130
128
131
132 def is_vcs_call(environ):
133 if VCS_TYPE_KEY in environ:
134 raw_type = environ[VCS_TYPE_KEY]
135 return raw_type and raw_type != VCS_TYPE_SKIP
136 return False
137
138
139 def detect_vcs_request(environ, backends):
140 checks = {
141 'hg': (is_hg, SimpleHg),
142 'git': (is_git, SimpleGit),
143 'svn': (is_svn, SimpleSvn),
144 }
145 handler = None
146
147 if VCS_TYPE_KEY in environ:
148 raw_type = environ[VCS_TYPE_KEY]
149 if raw_type == VCS_TYPE_SKIP:
150 log.debug('got `skip` marker for vcs detection, skipping...')
151 return handler
152
153 _check, handler = checks.get(raw_type) or [None, None]
154 if handler:
155 log.debug('got handler:%s from environ', handler)
156
157 if not handler:
158 log.debug('checking if request is of VCS type in order: %s', backends)
159 for vcs_type in backends:
160 vcs_check, _handler = checks[vcs_type]
161 if vcs_check(environ):
162 log.debug('vcs handler found %s', _handler)
163 handler = _handler
164 break
165
166 return handler
167
168
129 class VCSMiddleware(object):
169 class VCSMiddleware(object):
130
170
131 def __init__(self, app, config, appenlight_client, registry):
171 def __init__(self, app, config, appenlight_client, registry):
132 self.application = app
172 self.application = app
133 self.config = config
173 self.config = config
134 self.appenlight_client = appenlight_client
174 self.appenlight_client = appenlight_client
135 self.registry = registry
175 self.registry = registry
136 self.use_gzip = True
176 self.use_gzip = True
137 # order in which we check the middlewares, based on vcs.backends config
177 # order in which we check the middlewares, based on vcs.backends config
138 self.check_middlewares = config['vcs.backends']
178 self.check_middlewares = config['vcs.backends']
139 self.checks = {
140 'hg': (is_hg, SimpleHg),
141 'git': (is_git, SimpleGit),
142 'svn': (is_svn, SimpleSvn),
143 }
144
179
145 def vcs_config(self, repo_name=None):
180 def vcs_config(self, repo_name=None):
146 """
181 """
147 returns serialized VcsSettings
182 returns serialized VcsSettings
148 """
183 """
149 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
184 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
150
185
151 def wrap_in_gzip_if_enabled(self, app, config):
186 def wrap_in_gzip_if_enabled(self, app, config):
152 if self.use_gzip:
187 if self.use_gzip:
153 app = GunzipMiddleware(app)
188 app = GunzipMiddleware(app)
154 return app
189 return app
155
190
156 def _get_handler_app(self, environ):
191 def _get_handler_app(self, environ):
157 app = None
192 app = None
158 log.debug('Checking vcs types in order: %r', self.check_middlewares)
193 log.debug('VCSMiddleware: detecting vcs type.')
159 for vcs_type in self.check_middlewares:
194 handler = detect_vcs_request(environ, self.check_middlewares)
160 vcs_check, handler = self.checks[vcs_type]
195 if handler:
161 if vcs_check(environ):
196 app = handler(self.application, self.config, self.registry)
162 log.debug(
163 'Found VCS Middleware to handle the request %s', handler)
164 app = handler(self.application, self.config, self.registry)
165 break
166
197
167 return app
198 return app
168
199
169 def __call__(self, environ, start_response):
200 def __call__(self, environ, start_response):
170 # check if we handle one of interesting protocols, optionally extract
201 # check if we handle one of interesting protocols, optionally extract
171 # specific vcsSettings and allow changes of how things are wrapped
202 # specific vcsSettings and allow changes of how things are wrapped
172 vcs_handler = self._get_handler_app(environ)
203 vcs_handler = self._get_handler_app(environ)
173 if vcs_handler:
204 if vcs_handler:
174 # translate the _REPO_ID into real repo NAME for usage
205 # translate the _REPO_ID into real repo NAME for usage
175 # in middleware
206 # in middleware
176 environ['PATH_INFO'] = vcs_handler._get_by_id(environ['PATH_INFO'])
207 environ['PATH_INFO'] = vcs_handler._get_by_id(environ['PATH_INFO'])
177
208
178 # Set acl, url and vcs repo names.
209 # Set acl, url and vcs repo names.
179 vcs_handler.set_repo_names(environ)
210 vcs_handler.set_repo_names(environ)
180
211
181 # check for type, presence in database and on filesystem
212 # check for type, presence in database and on filesystem
182 if not vcs_handler.is_valid_and_existing_repo(
213 if not vcs_handler.is_valid_and_existing_repo(
183 vcs_handler.acl_repo_name,
214 vcs_handler.acl_repo_name,
184 vcs_handler.basepath,
215 vcs_handler.basepath,
185 vcs_handler.SCM):
216 vcs_handler.SCM):
186 return HTTPNotFound()(environ, start_response)
217 return HTTPNotFound()(environ, start_response)
187
218
188 # TODO: johbo: Needed for the Pyro4 backend and Mercurial only.
219 # TODO: johbo: Needed for the Pyro4 backend and Mercurial only.
189 # Remove once we fully switched to the HTTP backend.
220 # Remove once we fully switched to the HTTP backend.
190 environ['REPO_NAME'] = vcs_handler.url_repo_name
221 environ['REPO_NAME'] = vcs_handler.url_repo_name
191
222
192 # register repo config back to the handler
223 # register repo config back to the handler
193 vcs_handler.repo_vcs_config = self.vcs_config(
224 vcs_handler.repo_vcs_config = self.vcs_config(
194 vcs_handler.acl_repo_name)
225 vcs_handler.acl_repo_name)
195
226
196 # Wrap handler in middlewares if they are enabled.
227 # Wrap handler in middlewares if they are enabled.
197 vcs_handler = self.wrap_in_gzip_if_enabled(
228 vcs_handler = self.wrap_in_gzip_if_enabled(
198 vcs_handler, self.config)
229 vcs_handler, self.config)
199 vcs_handler, _ = wrap_in_appenlight_if_enabled(
230 vcs_handler, _ = wrap_in_appenlight_if_enabled(
200 vcs_handler, self.config, self.appenlight_client)
231 vcs_handler, self.config, self.appenlight_client)
201
232
202 return vcs_handler(environ, start_response)
233 return vcs_handler(environ, start_response)
203
234
204 return self.application(environ, start_response)
235 return self.application(environ, start_response)
@@ -1,81 +1,95 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import pylons
23 import pylons
24 import rhodecode
24 import rhodecode
25
25
26 from pylons.i18n.translation import _get_translator
26 from pylons.i18n.translation import _get_translator
27 from pylons.util import ContextObj
27 from pylons.util import ContextObj
28 from routes.util import URLGenerator
28 from routes.util import URLGenerator
29
29
30 from rhodecode.lib.base import attach_context_attributes, get_auth_user
30 from rhodecode.lib.base import attach_context_attributes, get_auth_user
31 from rhodecode.lib.middleware.vcs import (
32 detect_vcs_request, VCS_TYPE_KEY, VCS_TYPE_SKIP)
31 from rhodecode.model import meta
33 from rhodecode.model import meta
32
34
35
33 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
34
37
35
38
36 def pylons_compatibility_tween_factory(handler, registry):
39 def pylons_compatibility_tween_factory(handler, registry):
40
37 def pylons_compatibility_tween(request):
41 def pylons_compatibility_tween(request):
38 """
42 """
39 While migrating from pylons to pyramid we need to call some pylons code
43 While migrating from pylons to pyramid we need to call some pylons code
40 from pyramid. For example while rendering an old template that uses the
44 from pyramid. For example while rendering an old template that uses the
41 'c' or 'h' objects. This tween sets up the needed pylons globals.
45 'c' or 'h' objects. This tween sets up the needed pylons globals.
42 """
46 """
43 config = rhodecode.CONFIG
47 config = rhodecode.CONFIG
44 environ = request.environ
48 environ = request.environ
45 session = request.session
49 session = request.session
46 session_key = (config['pylons.environ_config']
50
47 .get('session', 'beaker.session'))
51 vcs_handler = detect_vcs_request(
52 request.environ, request.registry.settings.get('vcs.backends'))
53
54 if vcs_handler:
55 request.environ[VCS_TYPE_KEY] = vcs_handler.SCM
56 return handler(request)
57
58 request.environ[VCS_TYPE_KEY] = VCS_TYPE_SKIP
48
59
49 # Setup pylons globals.
60 # Setup pylons globals.
50 pylons.config._push_object(config)
61 pylons.config._push_object(config)
51 pylons.request._push_object(request)
62 pylons.request._push_object(request)
52 pylons.session._push_object(session)
63 pylons.session._push_object(session)
64
65 session_key = (
66 config['pylons.environ_config'].get('session', 'beaker.session'))
53 environ[session_key] = session
67 environ[session_key] = session
54 pylons.url._push_object(URLGenerator(config['routes.map'],
68 pylons.url._push_object(URLGenerator(config['routes.map'],
55 environ))
69 environ))
56
70
57 # TODO: Maybe we should use the language from pyramid.
71 # TODO: Maybe we should use the language from pyramid.
58 translator = _get_translator(config.get('lang'))
72 translator = _get_translator(config.get('lang'))
59 pylons.translator._push_object(translator)
73 pylons.translator._push_object(translator)
60
74
61 # Get the rhodecode auth user object and make it available.
75 # Get the rhodecode auth user object and make it available.
62 auth_user = get_auth_user(environ)
76 auth_user = get_auth_user(environ)
63 request.user = auth_user
77 request.user = auth_user
64 environ['rc_auth_user'] = auth_user
78 environ['rc_auth_user'] = auth_user
65
79
66 # Setup the pylons context object ('c')
80 # Setup the pylons context object ('c')
67 context = ContextObj()
81 context = ContextObj()
68 context.rhodecode_user = auth_user
82 context.rhodecode_user = auth_user
69 attach_context_attributes(context, request)
83 attach_context_attributes(context, request)
70 pylons.tmpl_context._push_object(context)
84 pylons.tmpl_context._push_object(context)
71 return handler(request)
85 return handler(request)
72
86
73 return pylons_compatibility_tween
87 return pylons_compatibility_tween
74
88
75
89
76 def includeme(config):
90 def includeme(config):
77 config.add_subscriber('rhodecode.subscribers.add_renderer_globals',
91 config.add_subscriber('rhodecode.subscribers.add_renderer_globals',
78 'pyramid.events.BeforeRender')
92 'pyramid.events.BeforeRender')
79 config.add_subscriber('rhodecode.subscribers.add_localizer',
93 config.add_subscriber('rhodecode.subscribers.add_localizer',
80 'pyramid.events.NewRequest')
94 'pyramid.events.NewRequest')
81 config.add_tween('rhodecode.tweens.pylons_compatibility_tween_factory')
95 config.add_tween('rhodecode.tweens.pylons_compatibility_tween_factory')
General Comments 0
You need to be logged in to leave comments. Login now