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