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