##// END OF EJS Templates
http: Allow http, pyro and custom implementations for scm app.
Martin Bornhold -
r962:a79a5a84 default
parent child Browse files
Show More
@@ -1,524 +1,527 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 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
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('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.get('vcs.scm_app_implementation')
176 custom_implementation = self.config['vcs.scm_app_implementation']
177 if custom_implementation and custom_implementation != 'pyro4':
177 if custom_implementation == 'http':
178 log.info(
178 log.info('Using HTTP implementation of scm app.')
179 "Using custom implementation of scm_app: %s",
179 scm_app_impl = scm_app_http
180 custom_implementation)
180 elif custom_implementation == 'pyro4':
181 log.info('Using Pyro implementation of scm app.')
182 scm_app_impl = scm_app
183 else:
184 log.info('Using custom implementation of scm_app: "{}"'.format(
185 custom_implementation))
181 scm_app_impl = importlib.import_module(custom_implementation)
186 scm_app_impl = importlib.import_module(custom_implementation)
182 else:
183 scm_app_impl = scm_app
184 return scm_app_impl
187 return scm_app_impl
185
188
186 def _get_by_id(self, repo_name):
189 def _get_by_id(self, repo_name):
187 """
190 """
188 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
189 with a repository_name for support of _<ID> non changeable urls
192 with a repository_name for support of _<ID> non changeable urls
190 """
193 """
191
194
192 data = repo_name.split('/')
195 data = repo_name.split('/')
193 if len(data) >= 2:
196 if len(data) >= 2:
194 from rhodecode.model.repo import RepoModel
197 from rhodecode.model.repo import RepoModel
195 by_id_match = RepoModel().get_repo_by_id(repo_name)
198 by_id_match = RepoModel().get_repo_by_id(repo_name)
196 if by_id_match:
199 if by_id_match:
197 data[1] = by_id_match.repo_name
200 data[1] = by_id_match.repo_name
198
201
199 return safe_str('/'.join(data))
202 return safe_str('/'.join(data))
200
203
201 def _invalidate_cache(self, repo_name):
204 def _invalidate_cache(self, repo_name):
202 """
205 """
203 Set's cache for this repository for invalidation on next access
206 Set's cache for this repository for invalidation on next access
204
207
205 :param repo_name: full repo name, also a cache key
208 :param repo_name: full repo name, also a cache key
206 """
209 """
207 ScmModel().mark_for_invalidation(repo_name)
210 ScmModel().mark_for_invalidation(repo_name)
208
211
209 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):
210 db_repo = Repository.get_by_repo_name(repo_name)
213 db_repo = Repository.get_by_repo_name(repo_name)
211 if not db_repo:
214 if not db_repo:
212 log.debug('Repository `%s` not found inside the database.',
215 log.debug('Repository `%s` not found inside the database.',
213 repo_name)
216 repo_name)
214 return False
217 return False
215
218
216 if db_repo.repo_type != scm_type:
219 if db_repo.repo_type != scm_type:
217 log.warning(
220 log.warning(
218 'Repository `%s` have incorrect scm_type, expected %s got %s',
221 'Repository `%s` have incorrect scm_type, expected %s got %s',
219 repo_name, db_repo.repo_type, scm_type)
222 repo_name, db_repo.repo_type, scm_type)
220 return False
223 return False
221
224
222 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
225 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
223
226
224 def valid_and_active_user(self, user):
227 def valid_and_active_user(self, user):
225 """
228 """
226 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
227 if he's active.
230 if he's active.
228
231
229 :param user: user object or None
232 :param user: user object or None
230 :return: boolean
233 :return: boolean
231 """
234 """
232 if user is None:
235 if user is None:
233 return False
236 return False
234
237
235 elif user.active:
238 elif user.active:
236 return True
239 return True
237
240
238 return False
241 return False
239
242
240 def _check_permission(self, action, user, repo_name, ip_addr=None):
243 def _check_permission(self, action, user, repo_name, ip_addr=None):
241 """
244 """
242 Checks permissions using action (push/pull) user and repository
245 Checks permissions using action (push/pull) user and repository
243 name
246 name
244
247
245 :param action: push or pull action
248 :param action: push or pull action
246 :param user: user instance
249 :param user: user instance
247 :param repo_name: repository name
250 :param repo_name: repository name
248 """
251 """
249 # check IP
252 # check IP
250 inherit = user.inherit_default_permissions
253 inherit = user.inherit_default_permissions
251 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
254 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
252 inherit_from_default=inherit)
255 inherit_from_default=inherit)
253 if ip_allowed:
256 if ip_allowed:
254 log.info('Access for IP:%s allowed', ip_addr)
257 log.info('Access for IP:%s allowed', ip_addr)
255 else:
258 else:
256 return False
259 return False
257
260
258 if action == 'push':
261 if action == 'push':
259 if not HasPermissionAnyMiddleware('repository.write',
262 if not HasPermissionAnyMiddleware('repository.write',
260 'repository.admin')(user,
263 'repository.admin')(user,
261 repo_name):
264 repo_name):
262 return False
265 return False
263
266
264 else:
267 else:
265 # any other action need at least read permission
268 # any other action need at least read permission
266 if not HasPermissionAnyMiddleware('repository.read',
269 if not HasPermissionAnyMiddleware('repository.read',
267 'repository.write',
270 'repository.write',
268 'repository.admin')(user,
271 'repository.admin')(user,
269 repo_name):
272 repo_name):
270 return False
273 return False
271
274
272 return True
275 return True
273
276
274 def _check_ssl(self, environ, start_response):
277 def _check_ssl(self, environ, start_response):
275 """
278 """
276 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
277 and required True otherwise
280 and required True otherwise
278 """
281 """
279 org_proto = environ['wsgi._org_proto']
282 org_proto = environ['wsgi._org_proto']
280 # 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 !
281 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
284 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
282 if require_ssl and org_proto == 'http':
285 if require_ssl and org_proto == 'http':
283 log.debug('proto is %s and SSL is required BAD REQUEST !',
286 log.debug('proto is %s and SSL is required BAD REQUEST !',
284 org_proto)
287 org_proto)
285 return False
288 return False
286 return True
289 return True
287
290
288 def __call__(self, environ, start_response):
291 def __call__(self, environ, start_response):
289 try:
292 try:
290 return self._handle_request(environ, start_response)
293 return self._handle_request(environ, start_response)
291 except Exception:
294 except Exception:
292 log.exception("Exception while handling request")
295 log.exception("Exception while handling request")
293 appenlight.track_exception(environ)
296 appenlight.track_exception(environ)
294 return HTTPInternalServerError()(environ, start_response)
297 return HTTPInternalServerError()(environ, start_response)
295 finally:
298 finally:
296 meta.Session.remove()
299 meta.Session.remove()
297
300
298 def _handle_request(self, environ, start_response):
301 def _handle_request(self, environ, start_response):
299
302
300 if not self._check_ssl(environ, start_response):
303 if not self._check_ssl(environ, start_response):
301 reason = ('SSL required, while RhodeCode was unable '
304 reason = ('SSL required, while RhodeCode was unable '
302 'to detect this as SSL request')
305 'to detect this as SSL request')
303 log.debug('User not allowed to proceed, %s', reason)
306 log.debug('User not allowed to proceed, %s', reason)
304 return HTTPNotAcceptable(reason)(environ, start_response)
307 return HTTPNotAcceptable(reason)(environ, start_response)
305
308
306 if not self.url_repo_name:
309 if not self.url_repo_name:
307 log.warning('Repository name is empty: %s', self.url_repo_name)
310 log.warning('Repository name is empty: %s', self.url_repo_name)
308 # failed to get repo name, we fail now
311 # failed to get repo name, we fail now
309 return HTTPNotFound()(environ, start_response)
312 return HTTPNotFound()(environ, start_response)
310 log.debug('Extracted repo name is %s', self.url_repo_name)
313 log.debug('Extracted repo name is %s', self.url_repo_name)
311
314
312 ip_addr = get_ip_addr(environ)
315 ip_addr = get_ip_addr(environ)
313 username = None
316 username = None
314
317
315 # skip passing error to error controller
318 # skip passing error to error controller
316 environ['pylons.status_code_redirect'] = True
319 environ['pylons.status_code_redirect'] = True
317
320
318 # ======================================================================
321 # ======================================================================
319 # GET ACTION PULL or PUSH
322 # GET ACTION PULL or PUSH
320 # ======================================================================
323 # ======================================================================
321 action = self._get_action(environ)
324 action = self._get_action(environ)
322
325
323 # ======================================================================
326 # ======================================================================
324 # 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.
325 # In this case only pull action is allowed.
328 # In this case only pull action is allowed.
326 # ======================================================================
329 # ======================================================================
327 if self.is_shadow_repo and action != 'pull':
330 if self.is_shadow_repo and action != 'pull':
328 reason = 'Only pull action is allowed for shadow repositories.'
331 reason = 'Only pull action is allowed for shadow repositories.'
329 log.debug('User not allowed to proceed, %s', reason)
332 log.debug('User not allowed to proceed, %s', reason)
330 return HTTPNotAcceptable(reason)(environ, start_response)
333 return HTTPNotAcceptable(reason)(environ, start_response)
331
334
332 # ======================================================================
335 # ======================================================================
333 # CHECK ANONYMOUS PERMISSION
336 # CHECK ANONYMOUS PERMISSION
334 # ======================================================================
337 # ======================================================================
335 if action in ['pull', 'push']:
338 if action in ['pull', 'push']:
336 anonymous_user = User.get_default_user()
339 anonymous_user = User.get_default_user()
337 username = anonymous_user.username
340 username = anonymous_user.username
338 if anonymous_user.active:
341 if anonymous_user.active:
339 # ONLY check permissions if the user is activated
342 # ONLY check permissions if the user is activated
340 anonymous_perm = self._check_permission(
343 anonymous_perm = self._check_permission(
341 action, anonymous_user, self.acl_repo_name, ip_addr)
344 action, anonymous_user, self.acl_repo_name, ip_addr)
342 else:
345 else:
343 anonymous_perm = False
346 anonymous_perm = False
344
347
345 if not anonymous_user.active or not anonymous_perm:
348 if not anonymous_user.active or not anonymous_perm:
346 if not anonymous_user.active:
349 if not anonymous_user.active:
347 log.debug('Anonymous access is disabled, running '
350 log.debug('Anonymous access is disabled, running '
348 'authentication')
351 'authentication')
349
352
350 if not anonymous_perm:
353 if not anonymous_perm:
351 log.debug('Not enough credentials to access this '
354 log.debug('Not enough credentials to access this '
352 'repository as anonymous user')
355 'repository as anonymous user')
353
356
354 username = None
357 username = None
355 # ==============================================================
358 # ==============================================================
356 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
359 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
357 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
360 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
358 # ==============================================================
361 # ==============================================================
359
362
360 # try to auth based on environ, container auth methods
363 # try to auth based on environ, container auth methods
361 log.debug('Running PRE-AUTH for container based authentication')
364 log.debug('Running PRE-AUTH for container based authentication')
362 pre_auth = authenticate(
365 pre_auth = authenticate(
363 '', '', environ, VCS_TYPE, registry=self.registry)
366 '', '', environ, VCS_TYPE, registry=self.registry)
364 if pre_auth and pre_auth.get('username'):
367 if pre_auth and pre_auth.get('username'):
365 username = pre_auth['username']
368 username = pre_auth['username']
366 log.debug('PRE-AUTH got %s as username', username)
369 log.debug('PRE-AUTH got %s as username', username)
367
370
368 # If not authenticated by the container, running basic auth
371 # If not authenticated by the container, running basic auth
369 if not username:
372 if not username:
370 self.authenticate.realm = get_rhodecode_realm()
373 self.authenticate.realm = get_rhodecode_realm()
371
374
372 try:
375 try:
373 result = self.authenticate(environ)
376 result = self.authenticate(environ)
374 except (UserCreationError, NotAllowedToCreateUserError) as e:
377 except (UserCreationError, NotAllowedToCreateUserError) as e:
375 log.error(e)
378 log.error(e)
376 reason = safe_str(e)
379 reason = safe_str(e)
377 return HTTPNotAcceptable(reason)(environ, start_response)
380 return HTTPNotAcceptable(reason)(environ, start_response)
378
381
379 if isinstance(result, str):
382 if isinstance(result, str):
380 AUTH_TYPE.update(environ, 'basic')
383 AUTH_TYPE.update(environ, 'basic')
381 REMOTE_USER.update(environ, result)
384 REMOTE_USER.update(environ, result)
382 username = result
385 username = result
383 else:
386 else:
384 return result.wsgi_application(environ, start_response)
387 return result.wsgi_application(environ, start_response)
385
388
386 # ==============================================================
389 # ==============================================================
387 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
390 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
388 # ==============================================================
391 # ==============================================================
389 user = User.get_by_username(username)
392 user = User.get_by_username(username)
390 if not self.valid_and_active_user(user):
393 if not self.valid_and_active_user(user):
391 return HTTPForbidden()(environ, start_response)
394 return HTTPForbidden()(environ, start_response)
392 username = user.username
395 username = user.username
393 user.update_lastactivity()
396 user.update_lastactivity()
394 meta.Session().commit()
397 meta.Session().commit()
395
398
396 # check user attributes for password change flag
399 # check user attributes for password change flag
397 user_obj = user
400 user_obj = user
398 if user_obj and user_obj.username != User.DEFAULT_USER and \
401 if user_obj and user_obj.username != User.DEFAULT_USER and \
399 user_obj.user_data.get('force_password_change'):
402 user_obj.user_data.get('force_password_change'):
400 reason = 'password change required'
403 reason = 'password change required'
401 log.debug('User not allowed to authenticate, %s', reason)
404 log.debug('User not allowed to authenticate, %s', reason)
402 return HTTPNotAcceptable(reason)(environ, start_response)
405 return HTTPNotAcceptable(reason)(environ, start_response)
403
406
404 # check permissions for this repository
407 # check permissions for this repository
405 perm = self._check_permission(
408 perm = self._check_permission(
406 action, user, self.acl_repo_name, ip_addr)
409 action, user, self.acl_repo_name, ip_addr)
407 if not perm:
410 if not perm:
408 return HTTPForbidden()(environ, start_response)
411 return HTTPForbidden()(environ, start_response)
409
412
410 # extras are injected into UI object and later available
413 # extras are injected into UI object and later available
411 # in hooks executed by rhodecode
414 # in hooks executed by rhodecode
412 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
415 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
413 extras = vcs_operation_context(
416 extras = vcs_operation_context(
414 environ, repo_name=self.acl_repo_name, username=username,
417 environ, repo_name=self.acl_repo_name, username=username,
415 action=action, scm=self.SCM, check_locking=check_locking,
418 action=action, scm=self.SCM, check_locking=check_locking,
416 is_shadow_repo=self.is_shadow_repo
419 is_shadow_repo=self.is_shadow_repo
417 )
420 )
418
421
419 # ======================================================================
422 # ======================================================================
420 # REQUEST HANDLING
423 # REQUEST HANDLING
421 # ======================================================================
424 # ======================================================================
422 repo_path = os.path.join(
425 repo_path = os.path.join(
423 safe_str(self.basepath), safe_str(self.vcs_repo_name))
426 safe_str(self.basepath), safe_str(self.vcs_repo_name))
424 log.debug('Repository path is %s', repo_path)
427 log.debug('Repository path is %s', repo_path)
425
428
426 fix_PATH()
429 fix_PATH()
427
430
428 log.info(
431 log.info(
429 '%s action on %s repo "%s" by "%s" from %s',
432 '%s action on %s repo "%s" by "%s" from %s',
430 action, self.SCM, safe_str(self.url_repo_name),
433 action, self.SCM, safe_str(self.url_repo_name),
431 safe_str(username), ip_addr)
434 safe_str(username), ip_addr)
432
435
433 return self._generate_vcs_response(
436 return self._generate_vcs_response(
434 environ, start_response, repo_path, extras, action)
437 environ, start_response, repo_path, extras, action)
435
438
436 @initialize_generator
439 @initialize_generator
437 def _generate_vcs_response(
440 def _generate_vcs_response(
438 self, environ, start_response, repo_path, extras, action):
441 self, environ, start_response, repo_path, extras, action):
439 """
442 """
440 Returns a generator for the response content.
443 Returns a generator for the response content.
441
444
442 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
443 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
444 also handles the locking exceptions which will be triggered when
447 also handles the locking exceptions which will be triggered when
445 the first chunk is produced by the underlying WSGI application.
448 the first chunk is produced by the underlying WSGI application.
446 """
449 """
447 callback_daemon, extras = self._prepare_callback_daemon(extras)
450 callback_daemon, extras = self._prepare_callback_daemon(extras)
448 config = self._create_config(extras, self.acl_repo_name)
451 config = self._create_config(extras, self.acl_repo_name)
449 log.debug('HOOKS extras is %s', extras)
452 log.debug('HOOKS extras is %s', extras)
450 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)
451
454
452 try:
455 try:
453 with callback_daemon:
456 with callback_daemon:
454 try:
457 try:
455 response = app(environ, start_response)
458 response = app(environ, start_response)
456 finally:
459 finally:
457 # This statement works together with the decorator
460 # This statement works together with the decorator
458 # "initialize_generator" above. The decorator ensures that
461 # "initialize_generator" above. The decorator ensures that
459 # we hit the first yield statement before the generator is
462 # we hit the first yield statement before the generator is
460 # returned back to the WSGI server. This is needed to
463 # returned back to the WSGI server. This is needed to
461 # ensure that the call to "app" above triggers the
464 # ensure that the call to "app" above triggers the
462 # needed callback to "start_response" before the
465 # needed callback to "start_response" before the
463 # generator is actually used.
466 # generator is actually used.
464 yield "__init__"
467 yield "__init__"
465
468
466 for chunk in response:
469 for chunk in response:
467 yield chunk
470 yield chunk
468 except Exception as exc:
471 except Exception as exc:
469 # TODO: johbo: Improve "translating" back the exception.
472 # TODO: johbo: Improve "translating" back the exception.
470 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
473 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
471 exc = HTTPLockedRC(*exc.args)
474 exc = HTTPLockedRC(*exc.args)
472 _code = rhodecode.CONFIG.get('lock_ret_code')
475 _code = rhodecode.CONFIG.get('lock_ret_code')
473 log.debug('Repository LOCKED ret code %s!', (_code,))
476 log.debug('Repository LOCKED ret code %s!', (_code,))
474 elif getattr(exc, '_vcs_kind', None) == 'requirement':
477 elif getattr(exc, '_vcs_kind', None) == 'requirement':
475 log.debug(
478 log.debug(
476 'Repository requires features unknown to this Mercurial')
479 'Repository requires features unknown to this Mercurial')
477 exc = HTTPRequirementError(*exc.args)
480 exc = HTTPRequirementError(*exc.args)
478 else:
481 else:
479 raise
482 raise
480
483
481 for chunk in exc(environ, start_response):
484 for chunk in exc(environ, start_response):
482 yield chunk
485 yield chunk
483 finally:
486 finally:
484 # invalidate cache on push
487 # invalidate cache on push
485 try:
488 try:
486 if action == 'push':
489 if action == 'push':
487 self._invalidate_cache(self.url_repo_name)
490 self._invalidate_cache(self.url_repo_name)
488 finally:
491 finally:
489 meta.Session.remove()
492 meta.Session.remove()
490
493
491 def _get_repository_name(self, environ):
494 def _get_repository_name(self, environ):
492 """Get repository name out of the environmnent
495 """Get repository name out of the environmnent
493
496
494 :param environ: WSGI environment
497 :param environ: WSGI environment
495 """
498 """
496 raise NotImplementedError()
499 raise NotImplementedError()
497
500
498 def _get_action(self, environ):
501 def _get_action(self, environ):
499 """Map request commands into a pull or push command.
502 """Map request commands into a pull or push command.
500
503
501 :param environ: WSGI environment
504 :param environ: WSGI environment
502 """
505 """
503 raise NotImplementedError()
506 raise NotImplementedError()
504
507
505 def _create_wsgi_app(self, repo_path, repo_name, config):
508 def _create_wsgi_app(self, repo_path, repo_name, config):
506 """Return the WSGI app that will finally handle the request."""
509 """Return the WSGI app that will finally handle the request."""
507 raise NotImplementedError()
510 raise NotImplementedError()
508
511
509 def _create_config(self, extras, repo_name):
512 def _create_config(self, extras, repo_name):
510 """Create a Pyro safe config representation."""
513 """Create a Pyro safe config representation."""
511 raise NotImplementedError()
514 raise NotImplementedError()
512
515
513 def _prepare_callback_daemon(self, extras):
516 def _prepare_callback_daemon(self, extras):
514 return prepare_callback_daemon(
517 return prepare_callback_daemon(
515 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
518 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
516 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
519 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
517
520
518
521
519 def _should_check_locking(query_string):
522 def _should_check_locking(query_string):
520 # this is kind of hacky, but due to how mercurial handles client-server
523 # this is kind of hacky, but due to how mercurial handles client-server
521 # server see all operation on commit; bookmarks, phases and
524 # server see all operation on commit; bookmarks, phases and
522 # obsolescence marker in different transaction, we don't want to check
525 # obsolescence marker in different transaction, we don't want to check
523 # locking on those
526 # locking on those
524 return query_string not in ['cmd=listkeys']
527 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now