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