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