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