##// END OF EJS Templates
http: improved log for ssl required
marcink -
r2593:b3b99584 default
parent child Browse files
Show More
@@ -1,645 +1,648 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 re
27 import re
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31
31
32 import time
32 import time
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 # TODO(marcink): check if we should use webob.exc here ?
34 # TODO(marcink): check if we should use webob.exc here ?
35 from pyramid.httpexceptions import (
35 from pyramid.httpexceptions import (
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 from zope.cachedescriptors.property import Lazy as LazyProperty
37 from zope.cachedescriptors.property import Lazy as LazyProperty
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.authentication.base import (
40 from rhodecode.authentication.base import (
41 authenticate, get_perms_cache_manager, VCS_TYPE, loadplugin)
41 authenticate, get_perms_cache_manager, VCS_TYPE, loadplugin)
42 from rhodecode.lib import caches
42 from rhodecode.lib import caches
43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.base import (
44 from rhodecode.lib.base import (
45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 from rhodecode.lib.exceptions import (
46 from rhodecode.lib.exceptions import (
47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
48 NotAllowedToCreateUserError)
48 NotAllowedToCreateUserError)
49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
50 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware import appenlight
51 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.middleware.utils import scm_app_http
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 from rhodecode.lib.vcs.backends import base
55 from rhodecode.lib.vcs.backends import base
56 from rhodecode.model import meta
56 from rhodecode.model import meta
57 from rhodecode.model.db import User, Repository, PullRequest
57 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def initialize_generator(factory):
65 def initialize_generator(factory):
66 """
66 """
67 Initializes the returned generator by draining its first element.
67 Initializes the returned generator by draining its first element.
68
68
69 This can be used to give a generator an initializer, which is the code
69 This can be used to give a generator an initializer, which is the code
70 up to the first yield statement. This decorator enforces that the first
70 up to the first yield statement. This decorator enforces that the first
71 produced element has the value ``"__init__"`` to make its special
71 produced element has the value ``"__init__"`` to make its special
72 purpose very explicit in the using code.
72 purpose very explicit in the using code.
73 """
73 """
74
74
75 @wraps(factory)
75 @wraps(factory)
76 def wrapper(*args, **kwargs):
76 def wrapper(*args, **kwargs):
77 gen = factory(*args, **kwargs)
77 gen = factory(*args, **kwargs)
78 try:
78 try:
79 init = gen.next()
79 init = gen.next()
80 except StopIteration:
80 except StopIteration:
81 raise ValueError('Generator must yield at least one element.')
81 raise ValueError('Generator must yield at least one element.')
82 if init != "__init__":
82 if init != "__init__":
83 raise ValueError('First yielded element must be "__init__".')
83 raise ValueError('First yielded element must be "__init__".')
84 return gen
84 return gen
85 return wrapper
85 return wrapper
86
86
87
87
88 class SimpleVCS(object):
88 class SimpleVCS(object):
89 """Common functionality for SCM HTTP handlers."""
89 """Common functionality for SCM HTTP handlers."""
90
90
91 SCM = 'unknown'
91 SCM = 'unknown'
92
92
93 acl_repo_name = None
93 acl_repo_name = None
94 url_repo_name = None
94 url_repo_name = None
95 vcs_repo_name = None
95 vcs_repo_name = None
96 rc_extras = {}
96 rc_extras = {}
97
97
98 # We have to handle requests to shadow repositories different than requests
98 # We have to handle requests to shadow repositories different than requests
99 # to normal repositories. Therefore we have to distinguish them. To do this
99 # to normal repositories. Therefore we have to distinguish them. To do this
100 # we use this regex which will match only on URLs pointing to shadow
100 # we use this regex which will match only on URLs pointing to shadow
101 # repositories.
101 # repositories.
102 shadow_repo_re = re.compile(
102 shadow_repo_re = re.compile(
103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
104 '(?P<target>{slug_pat})/' # target repo
104 '(?P<target>{slug_pat})/' # target repo
105 'pull-request/(?P<pr_id>\d+)/' # pull request
105 'pull-request/(?P<pr_id>\d+)/' # pull request
106 'repository$' # shadow repo
106 'repository$' # shadow repo
107 .format(slug_pat=SLUG_RE.pattern))
107 .format(slug_pat=SLUG_RE.pattern))
108
108
109 def __init__(self, config, registry):
109 def __init__(self, config, registry):
110 self.registry = registry
110 self.registry = registry
111 self.config = config
111 self.config = config
112 # re-populated by specialized middleware
112 # re-populated by specialized middleware
113 self.repo_vcs_config = base.Config()
113 self.repo_vcs_config = base.Config()
114 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
114 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
115
115
116 registry.rhodecode_settings = self.rhodecode_settings
116 registry.rhodecode_settings = self.rhodecode_settings
117 # authenticate this VCS request using authfunc
117 # authenticate this VCS request using authfunc
118 auth_ret_code_detection = \
118 auth_ret_code_detection = \
119 str2bool(self.config.get('auth_ret_code_detection', False))
119 str2bool(self.config.get('auth_ret_code_detection', False))
120 self.authenticate = BasicAuth(
120 self.authenticate = BasicAuth(
121 '', authenticate, registry, config.get('auth_ret_code'),
121 '', authenticate, registry, config.get('auth_ret_code'),
122 auth_ret_code_detection)
122 auth_ret_code_detection)
123 self.ip_addr = '0.0.0.0'
123 self.ip_addr = '0.0.0.0'
124
124
125 @LazyProperty
125 @LazyProperty
126 def global_vcs_config(self):
126 def global_vcs_config(self):
127 try:
127 try:
128 return VcsSettingsModel().get_ui_settings_as_config_obj()
128 return VcsSettingsModel().get_ui_settings_as_config_obj()
129 except Exception:
129 except Exception:
130 return base.Config()
130 return base.Config()
131
131
132 @property
132 @property
133 def base_path(self):
133 def base_path(self):
134 settings_path = self.repo_vcs_config.get(
134 settings_path = self.repo_vcs_config.get(
135 *VcsSettingsModel.PATH_SETTING)
135 *VcsSettingsModel.PATH_SETTING)
136
136
137 if not settings_path:
137 if not settings_path:
138 settings_path = self.global_vcs_config.get(
138 settings_path = self.global_vcs_config.get(
139 *VcsSettingsModel.PATH_SETTING)
139 *VcsSettingsModel.PATH_SETTING)
140
140
141 if not settings_path:
141 if not settings_path:
142 # try, maybe we passed in explicitly as config option
142 # try, maybe we passed in explicitly as config option
143 settings_path = self.config.get('base_path')
143 settings_path = self.config.get('base_path')
144
144
145 if not settings_path:
145 if not settings_path:
146 raise ValueError('FATAL: base_path is empty')
146 raise ValueError('FATAL: base_path is empty')
147 return settings_path
147 return settings_path
148
148
149 def set_repo_names(self, environ):
149 def set_repo_names(self, environ):
150 """
150 """
151 This will populate the attributes acl_repo_name, url_repo_name,
151 This will populate the attributes acl_repo_name, url_repo_name,
152 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
152 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
153 shadow) repositories all names are equal. In case of requests to a
153 shadow) repositories all names are equal. In case of requests to a
154 shadow repository the acl-name points to the target repo of the pull
154 shadow repository the acl-name points to the target repo of the pull
155 request and the vcs-name points to the shadow repo file system path.
155 request and the vcs-name points to the shadow repo file system path.
156 The url-name is always the URL used by the vcs client program.
156 The url-name is always the URL used by the vcs client program.
157
157
158 Example in case of a shadow repo:
158 Example in case of a shadow repo:
159 acl_repo_name = RepoGroup/MyRepo
159 acl_repo_name = RepoGroup/MyRepo
160 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
160 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
161 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
161 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
162 """
162 """
163 # First we set the repo name from URL for all attributes. This is the
163 # First we set the repo name from URL for all attributes. This is the
164 # default if handling normal (non shadow) repo requests.
164 # default if handling normal (non shadow) repo requests.
165 self.url_repo_name = self._get_repository_name(environ)
165 self.url_repo_name = self._get_repository_name(environ)
166 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
166 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
167 self.is_shadow_repo = False
167 self.is_shadow_repo = False
168
168
169 # Check if this is a request to a shadow repository.
169 # Check if this is a request to a shadow repository.
170 match = self.shadow_repo_re.match(self.url_repo_name)
170 match = self.shadow_repo_re.match(self.url_repo_name)
171 if match:
171 if match:
172 match_dict = match.groupdict()
172 match_dict = match.groupdict()
173
173
174 # Build acl repo name from regex match.
174 # Build acl repo name from regex match.
175 acl_repo_name = safe_unicode('{groups}{target}'.format(
175 acl_repo_name = safe_unicode('{groups}{target}'.format(
176 groups=match_dict['groups'] or '',
176 groups=match_dict['groups'] or '',
177 target=match_dict['target']))
177 target=match_dict['target']))
178
178
179 # Retrieve pull request instance by ID from regex match.
179 # Retrieve pull request instance by ID from regex match.
180 pull_request = PullRequest.get(match_dict['pr_id'])
180 pull_request = PullRequest.get(match_dict['pr_id'])
181
181
182 # Only proceed if we got a pull request and if acl repo name from
182 # Only proceed if we got a pull request and if acl repo name from
183 # URL equals the target repo name of the pull request.
183 # URL equals the target repo name of the pull request.
184 if pull_request and (acl_repo_name ==
184 if pull_request and (acl_repo_name ==
185 pull_request.target_repo.repo_name):
185 pull_request.target_repo.repo_name):
186 # Get file system path to shadow repository.
186 # Get file system path to shadow repository.
187 workspace_id = PullRequestModel()._workspace_id(pull_request)
187 workspace_id = PullRequestModel()._workspace_id(pull_request)
188 target_vcs = pull_request.target_repo.scm_instance()
188 target_vcs = pull_request.target_repo.scm_instance()
189 vcs_repo_name = target_vcs._get_shadow_repository_path(
189 vcs_repo_name = target_vcs._get_shadow_repository_path(
190 workspace_id)
190 workspace_id)
191
191
192 # Store names for later usage.
192 # Store names for later usage.
193 self.vcs_repo_name = vcs_repo_name
193 self.vcs_repo_name = vcs_repo_name
194 self.acl_repo_name = acl_repo_name
194 self.acl_repo_name = acl_repo_name
195 self.is_shadow_repo = True
195 self.is_shadow_repo = True
196
196
197 log.debug('Setting all VCS repository names: %s', {
197 log.debug('Setting all VCS repository names: %s', {
198 'acl_repo_name': self.acl_repo_name,
198 'acl_repo_name': self.acl_repo_name,
199 'url_repo_name': self.url_repo_name,
199 'url_repo_name': self.url_repo_name,
200 'vcs_repo_name': self.vcs_repo_name,
200 'vcs_repo_name': self.vcs_repo_name,
201 })
201 })
202
202
203 @property
203 @property
204 def scm_app(self):
204 def scm_app(self):
205 custom_implementation = self.config['vcs.scm_app_implementation']
205 custom_implementation = self.config['vcs.scm_app_implementation']
206 if custom_implementation == 'http':
206 if custom_implementation == 'http':
207 log.info('Using HTTP implementation of scm app.')
207 log.info('Using HTTP implementation of scm app.')
208 scm_app_impl = scm_app_http
208 scm_app_impl = scm_app_http
209 else:
209 else:
210 log.info('Using custom implementation of scm_app: "{}"'.format(
210 log.info('Using custom implementation of scm_app: "{}"'.format(
211 custom_implementation))
211 custom_implementation))
212 scm_app_impl = importlib.import_module(custom_implementation)
212 scm_app_impl = importlib.import_module(custom_implementation)
213 return scm_app_impl
213 return scm_app_impl
214
214
215 def _get_by_id(self, repo_name):
215 def _get_by_id(self, repo_name):
216 """
216 """
217 Gets a special pattern _<ID> from clone url and tries to replace it
217 Gets a special pattern _<ID> from clone url and tries to replace it
218 with a repository_name for support of _<ID> non changeable urls
218 with a repository_name for support of _<ID> non changeable urls
219 """
219 """
220
220
221 data = repo_name.split('/')
221 data = repo_name.split('/')
222 if len(data) >= 2:
222 if len(data) >= 2:
223 from rhodecode.model.repo import RepoModel
223 from rhodecode.model.repo import RepoModel
224 by_id_match = RepoModel().get_repo_by_id(repo_name)
224 by_id_match = RepoModel().get_repo_by_id(repo_name)
225 if by_id_match:
225 if by_id_match:
226 data[1] = by_id_match.repo_name
226 data[1] = by_id_match.repo_name
227
227
228 return safe_str('/'.join(data))
228 return safe_str('/'.join(data))
229
229
230 def _invalidate_cache(self, repo_name):
230 def _invalidate_cache(self, repo_name):
231 """
231 """
232 Set's cache for this repository for invalidation on next access
232 Set's cache for this repository for invalidation on next access
233
233
234 :param repo_name: full repo name, also a cache key
234 :param repo_name: full repo name, also a cache key
235 """
235 """
236 ScmModel().mark_for_invalidation(repo_name)
236 ScmModel().mark_for_invalidation(repo_name)
237
237
238 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
238 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
239 db_repo = Repository.get_by_repo_name(repo_name)
239 db_repo = Repository.get_by_repo_name(repo_name)
240 if not db_repo:
240 if not db_repo:
241 log.debug('Repository `%s` not found inside the database.',
241 log.debug('Repository `%s` not found inside the database.',
242 repo_name)
242 repo_name)
243 return False
243 return False
244
244
245 if db_repo.repo_type != scm_type:
245 if db_repo.repo_type != scm_type:
246 log.warning(
246 log.warning(
247 'Repository `%s` have incorrect scm_type, expected %s got %s',
247 'Repository `%s` have incorrect scm_type, expected %s got %s',
248 repo_name, db_repo.repo_type, scm_type)
248 repo_name, db_repo.repo_type, scm_type)
249 return False
249 return False
250
250
251 config = db_repo._config
251 config = db_repo._config
252 config.set('extensions', 'largefiles', '')
252 config.set('extensions', 'largefiles', '')
253 return is_valid_repo(
253 return is_valid_repo(
254 repo_name, base_path,
254 repo_name, base_path,
255 explicit_scm=scm_type, expect_scm=scm_type, config=config)
255 explicit_scm=scm_type, expect_scm=scm_type, config=config)
256
256
257 def valid_and_active_user(self, user):
257 def valid_and_active_user(self, user):
258 """
258 """
259 Checks if that user is not empty, and if it's actually object it checks
259 Checks if that user is not empty, and if it's actually object it checks
260 if he's active.
260 if he's active.
261
261
262 :param user: user object or None
262 :param user: user object or None
263 :return: boolean
263 :return: boolean
264 """
264 """
265 if user is None:
265 if user is None:
266 return False
266 return False
267
267
268 elif user.active:
268 elif user.active:
269 return True
269 return True
270
270
271 return False
271 return False
272
272
273 @property
273 @property
274 def is_shadow_repo_dir(self):
274 def is_shadow_repo_dir(self):
275 return os.path.isdir(self.vcs_repo_name)
275 return os.path.isdir(self.vcs_repo_name)
276
276
277 def _check_permission(self, action, user, repo_name, ip_addr=None,
277 def _check_permission(self, action, user, repo_name, ip_addr=None,
278 plugin_id='', plugin_cache_active=False, cache_ttl=0):
278 plugin_id='', plugin_cache_active=False, cache_ttl=0):
279 """
279 """
280 Checks permissions using action (push/pull) user and repository
280 Checks permissions using action (push/pull) user and repository
281 name. If plugin_cache and ttl is set it will use the plugin which
281 name. If plugin_cache and ttl is set it will use the plugin which
282 authenticated the user to store the cached permissions result for N
282 authenticated the user to store the cached permissions result for N
283 amount of seconds as in cache_ttl
283 amount of seconds as in cache_ttl
284
284
285 :param action: push or pull action
285 :param action: push or pull action
286 :param user: user instance
286 :param user: user instance
287 :param repo_name: repository name
287 :param repo_name: repository name
288 """
288 """
289
289
290 # get instance of cache manager configured for a namespace
290 # get instance of cache manager configured for a namespace
291 cache_manager = get_perms_cache_manager(
291 cache_manager = get_perms_cache_manager(
292 custom_ttl=cache_ttl, suffix=user.user_id)
292 custom_ttl=cache_ttl, suffix=user.user_id)
293 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
293 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
294 plugin_id, plugin_cache_active, cache_ttl)
294 plugin_id, plugin_cache_active, cache_ttl)
295
295
296 # for environ based password can be empty, but then the validation is
296 # for environ based password can be empty, but then the validation is
297 # on the server that fills in the env data needed for authentication
297 # on the server that fills in the env data needed for authentication
298 _perm_calc_hash = caches.compute_key_from_params(
298 _perm_calc_hash = caches.compute_key_from_params(
299 plugin_id, action, user.user_id, repo_name, ip_addr)
299 plugin_id, action, user.user_id, repo_name, ip_addr)
300
300
301 # _authenticate is a wrapper for .auth() method of plugin.
301 # _authenticate is a wrapper for .auth() method of plugin.
302 # it checks if .auth() sends proper data.
302 # it checks if .auth() sends proper data.
303 # For RhodeCodeExternalAuthPlugin it also maps users to
303 # For RhodeCodeExternalAuthPlugin it also maps users to
304 # Database and maps the attributes returned from .auth()
304 # Database and maps the attributes returned from .auth()
305 # to RhodeCode database. If this function returns data
305 # to RhodeCode database. If this function returns data
306 # then auth is correct.
306 # then auth is correct.
307 start = time.time()
307 start = time.time()
308 log.debug('Running plugin `%s` permissions check', plugin_id)
308 log.debug('Running plugin `%s` permissions check', plugin_id)
309
309
310 def perm_func():
310 def perm_func():
311 """
311 """
312 This function is used internally in Cache of Beaker to calculate
312 This function is used internally in Cache of Beaker to calculate
313 Results
313 Results
314 """
314 """
315 log.debug('auth: calculating permission access now...')
315 log.debug('auth: calculating permission access now...')
316 # check IP
316 # check IP
317 inherit = user.inherit_default_permissions
317 inherit = user.inherit_default_permissions
318 ip_allowed = AuthUser.check_ip_allowed(
318 ip_allowed = AuthUser.check_ip_allowed(
319 user.user_id, ip_addr, inherit_from_default=inherit)
319 user.user_id, ip_addr, inherit_from_default=inherit)
320 if ip_allowed:
320 if ip_allowed:
321 log.info('Access for IP:%s allowed', ip_addr)
321 log.info('Access for IP:%s allowed', ip_addr)
322 else:
322 else:
323 return False
323 return False
324
324
325 if action == 'push':
325 if action == 'push':
326 perms = ('repository.write', 'repository.admin')
326 perms = ('repository.write', 'repository.admin')
327 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
327 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
328 return False
328 return False
329
329
330 else:
330 else:
331 # any other action need at least read permission
331 # any other action need at least read permission
332 perms = (
332 perms = (
333 'repository.read', 'repository.write', 'repository.admin')
333 'repository.read', 'repository.write', 'repository.admin')
334 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
334 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
335 return False
335 return False
336
336
337 return True
337 return True
338
338
339 if plugin_cache_active:
339 if plugin_cache_active:
340 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
340 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
341 perm_result = cache_manager.get(
341 perm_result = cache_manager.get(
342 _perm_calc_hash, createfunc=perm_func)
342 _perm_calc_hash, createfunc=perm_func)
343 else:
343 else:
344 perm_result = perm_func()
344 perm_result = perm_func()
345
345
346 auth_time = time.time() - start
346 auth_time = time.time() - start
347 log.debug('Permissions for plugin `%s` completed in %.3fs, '
347 log.debug('Permissions for plugin `%s` completed in %.3fs, '
348 'expiration time of fetched cache %.1fs.',
348 'expiration time of fetched cache %.1fs.',
349 plugin_id, auth_time, cache_ttl)
349 plugin_id, auth_time, cache_ttl)
350
350
351 return perm_result
351 return perm_result
352
352
353 def _check_ssl(self, environ, start_response):
353 def _check_ssl(self, environ, start_response):
354 """
354 """
355 Checks the SSL check flag and returns False if SSL is not present
355 Checks the SSL check flag and returns False if SSL is not present
356 and required True otherwise
356 and required True otherwise
357 """
357 """
358 org_proto = environ['wsgi._org_proto']
358 org_proto = environ['wsgi._org_proto']
359 # check if we have SSL required ! if not it's a bad request !
359 # check if we have SSL required ! if not it's a bad request !
360 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
360 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
361 if require_ssl and org_proto == 'http':
361 if require_ssl and org_proto == 'http':
362 log.debug('proto is %s and SSL is required BAD REQUEST !',
362 log.debug(
363 org_proto)
363 'Bad request: detected protocol is `%s` and '
364 'SSL/HTTPS is required.', org_proto)
364 return False
365 return False
365 return True
366 return True
366
367
367 def _get_default_cache_ttl(self):
368 def _get_default_cache_ttl(self):
368 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
369 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
369 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
370 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
370 plugin_settings = plugin.get_settings()
371 plugin_settings = plugin.get_settings()
371 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
372 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
372 plugin_settings) or (False, 0)
373 plugin_settings) or (False, 0)
373 return plugin_cache_active, cache_ttl
374 return plugin_cache_active, cache_ttl
374
375
375 def __call__(self, environ, start_response):
376 def __call__(self, environ, start_response):
376 try:
377 try:
377 return self._handle_request(environ, start_response)
378 return self._handle_request(environ, start_response)
378 except Exception:
379 except Exception:
379 log.exception("Exception while handling request")
380 log.exception("Exception while handling request")
380 appenlight.track_exception(environ)
381 appenlight.track_exception(environ)
381 return HTTPInternalServerError()(environ, start_response)
382 return HTTPInternalServerError()(environ, start_response)
382 finally:
383 finally:
383 meta.Session.remove()
384 meta.Session.remove()
384
385
385 def _handle_request(self, environ, start_response):
386 def _handle_request(self, environ, start_response):
386
387
387 if not self._check_ssl(environ, start_response):
388 if not self._check_ssl(environ, start_response):
388 reason = ('SSL required, while RhodeCode was unable '
389 reason = ('SSL required, while RhodeCode was unable '
389 'to detect this as SSL request')
390 'to detect this as SSL request')
390 log.debug('User not allowed to proceed, %s', reason)
391 log.debug('User not allowed to proceed, %s', reason)
391 return HTTPNotAcceptable(reason)(environ, start_response)
392 return HTTPNotAcceptable(reason)(environ, start_response)
392
393
393 if not self.url_repo_name:
394 if not self.url_repo_name:
394 log.warning('Repository name is empty: %s', self.url_repo_name)
395 log.warning('Repository name is empty: %s', self.url_repo_name)
395 # failed to get repo name, we fail now
396 # failed to get repo name, we fail now
396 return HTTPNotFound()(environ, start_response)
397 return HTTPNotFound()(environ, start_response)
397 log.debug('Extracted repo name is %s', self.url_repo_name)
398 log.debug('Extracted repo name is %s', self.url_repo_name)
398
399
399 ip_addr = get_ip_addr(environ)
400 ip_addr = get_ip_addr(environ)
400 user_agent = get_user_agent(environ)
401 user_agent = get_user_agent(environ)
401 username = None
402 username = None
402
403
403 # skip passing error to error controller
404 # skip passing error to error controller
404 environ['pylons.status_code_redirect'] = True
405 environ['pylons.status_code_redirect'] = True
405
406
406 # ======================================================================
407 # ======================================================================
407 # GET ACTION PULL or PUSH
408 # GET ACTION PULL or PUSH
408 # ======================================================================
409 # ======================================================================
409 action = self._get_action(environ)
410 action = self._get_action(environ)
410
411
411 # ======================================================================
412 # ======================================================================
412 # Check if this is a request to a shadow repository of a pull request.
413 # Check if this is a request to a shadow repository of a pull request.
413 # In this case only pull action is allowed.
414 # In this case only pull action is allowed.
414 # ======================================================================
415 # ======================================================================
415 if self.is_shadow_repo and action != 'pull':
416 if self.is_shadow_repo and action != 'pull':
416 reason = 'Only pull action is allowed for shadow repositories.'
417 reason = 'Only pull action is allowed for shadow repositories.'
417 log.debug('User not allowed to proceed, %s', reason)
418 log.debug('User not allowed to proceed, %s', reason)
418 return HTTPNotAcceptable(reason)(environ, start_response)
419 return HTTPNotAcceptable(reason)(environ, start_response)
419
420
420 # Check if the shadow repo actually exists, in case someone refers
421 # Check if the shadow repo actually exists, in case someone refers
421 # to it, and it has been deleted because of successful merge.
422 # to it, and it has been deleted because of successful merge.
422 if self.is_shadow_repo and not self.is_shadow_repo_dir:
423 if self.is_shadow_repo and not self.is_shadow_repo_dir:
423 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
424 log.debug(
424 self.is_shadow_repo_dir)
425 'Shadow repo detected, and shadow repo dir `%s` is missing',
426 self.is_shadow_repo_dir)
425 return HTTPNotFound()(environ, start_response)
427 return HTTPNotFound()(environ, start_response)
426
428
427 # ======================================================================
429 # ======================================================================
428 # CHECK ANONYMOUS PERMISSION
430 # CHECK ANONYMOUS PERMISSION
429 # ======================================================================
431 # ======================================================================
430 if action in ['pull', 'push']:
432 if action in ['pull', 'push']:
431 anonymous_user = User.get_default_user()
433 anonymous_user = User.get_default_user()
432 username = anonymous_user.username
434 username = anonymous_user.username
433 if anonymous_user.active:
435 if anonymous_user.active:
434 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
436 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
435 # ONLY check permissions if the user is activated
437 # ONLY check permissions if the user is activated
436 anonymous_perm = self._check_permission(
438 anonymous_perm = self._check_permission(
437 action, anonymous_user, self.acl_repo_name, ip_addr,
439 action, anonymous_user, self.acl_repo_name, ip_addr,
438 plugin_id='anonymous_access',
440 plugin_id='anonymous_access',
439 plugin_cache_active=plugin_cache_active, cache_ttl=cache_ttl,
441 plugin_cache_active=plugin_cache_active,
442 cache_ttl=cache_ttl,
440 )
443 )
441 else:
444 else:
442 anonymous_perm = False
445 anonymous_perm = False
443
446
444 if not anonymous_user.active or not anonymous_perm:
447 if not anonymous_user.active or not anonymous_perm:
445 if not anonymous_user.active:
448 if not anonymous_user.active:
446 log.debug('Anonymous access is disabled, running '
449 log.debug('Anonymous access is disabled, running '
447 'authentication')
450 'authentication')
448
451
449 if not anonymous_perm:
452 if not anonymous_perm:
450 log.debug('Not enough credentials to access this '
453 log.debug('Not enough credentials to access this '
451 'repository as anonymous user')
454 'repository as anonymous user')
452
455
453 username = None
456 username = None
454 # ==============================================================
457 # ==============================================================
455 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
458 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
456 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
459 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
457 # ==============================================================
460 # ==============================================================
458
461
459 # try to auth based on environ, container auth methods
462 # try to auth based on environ, container auth methods
460 log.debug('Running PRE-AUTH for container based authentication')
463 log.debug('Running PRE-AUTH for container based authentication')
461 pre_auth = authenticate(
464 pre_auth = authenticate(
462 '', '', environ, VCS_TYPE, registry=self.registry,
465 '', '', environ, VCS_TYPE, registry=self.registry,
463 acl_repo_name=self.acl_repo_name)
466 acl_repo_name=self.acl_repo_name)
464 if pre_auth and pre_auth.get('username'):
467 if pre_auth and pre_auth.get('username'):
465 username = pre_auth['username']
468 username = pre_auth['username']
466 log.debug('PRE-AUTH got %s as username', username)
469 log.debug('PRE-AUTH got %s as username', username)
467 if pre_auth:
470 if pre_auth:
468 log.debug('PRE-AUTH successful from %s',
471 log.debug('PRE-AUTH successful from %s',
469 pre_auth.get('auth_data', {}).get('_plugin'))
472 pre_auth.get('auth_data', {}).get('_plugin'))
470
473
471 # If not authenticated by the container, running basic auth
474 # If not authenticated by the container, running basic auth
472 # before inject the calling repo_name for special scope checks
475 # before inject the calling repo_name for special scope checks
473 self.authenticate.acl_repo_name = self.acl_repo_name
476 self.authenticate.acl_repo_name = self.acl_repo_name
474
477
475 plugin_cache_active, cache_ttl = False, 0
478 plugin_cache_active, cache_ttl = False, 0
476 plugin = None
479 plugin = None
477 if not username:
480 if not username:
478 self.authenticate.realm = self.authenticate.get_rc_realm()
481 self.authenticate.realm = self.authenticate.get_rc_realm()
479
482
480 try:
483 try:
481 auth_result = self.authenticate(environ)
484 auth_result = self.authenticate(environ)
482 except (UserCreationError, NotAllowedToCreateUserError) as e:
485 except (UserCreationError, NotAllowedToCreateUserError) as e:
483 log.error(e)
486 log.error(e)
484 reason = safe_str(e)
487 reason = safe_str(e)
485 return HTTPNotAcceptable(reason)(environ, start_response)
488 return HTTPNotAcceptable(reason)(environ, start_response)
486
489
487 if isinstance(auth_result, dict):
490 if isinstance(auth_result, dict):
488 AUTH_TYPE.update(environ, 'basic')
491 AUTH_TYPE.update(environ, 'basic')
489 REMOTE_USER.update(environ, auth_result['username'])
492 REMOTE_USER.update(environ, auth_result['username'])
490 username = auth_result['username']
493 username = auth_result['username']
491 plugin = auth_result.get('auth_data', {}).get('_plugin')
494 plugin = auth_result.get('auth_data', {}).get('_plugin')
492 log.info(
495 log.info(
493 'MAIN-AUTH successful for user `%s` from %s plugin',
496 'MAIN-AUTH successful for user `%s` from %s plugin',
494 username, plugin)
497 username, plugin)
495
498
496 plugin_cache_active, cache_ttl = auth_result.get(
499 plugin_cache_active, cache_ttl = auth_result.get(
497 'auth_data', {}).get('_ttl_cache') or (False, 0)
500 'auth_data', {}).get('_ttl_cache') or (False, 0)
498 else:
501 else:
499 return auth_result.wsgi_application(
502 return auth_result.wsgi_application(
500 environ, start_response)
503 environ, start_response)
501
504
502
505
503 # ==============================================================
506 # ==============================================================
504 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
507 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
505 # ==============================================================
508 # ==============================================================
506 user = User.get_by_username(username)
509 user = User.get_by_username(username)
507 if not self.valid_and_active_user(user):
510 if not self.valid_and_active_user(user):
508 return HTTPForbidden()(environ, start_response)
511 return HTTPForbidden()(environ, start_response)
509 username = user.username
512 username = user.username
510 user.update_lastactivity()
513 user.update_lastactivity()
511 meta.Session().commit()
514 meta.Session().commit()
512
515
513 # check user attributes for password change flag
516 # check user attributes for password change flag
514 user_obj = user
517 user_obj = user
515 if user_obj and user_obj.username != User.DEFAULT_USER and \
518 if user_obj and user_obj.username != User.DEFAULT_USER and \
516 user_obj.user_data.get('force_password_change'):
519 user_obj.user_data.get('force_password_change'):
517 reason = 'password change required'
520 reason = 'password change required'
518 log.debug('User not allowed to authenticate, %s', reason)
521 log.debug('User not allowed to authenticate, %s', reason)
519 return HTTPNotAcceptable(reason)(environ, start_response)
522 return HTTPNotAcceptable(reason)(environ, start_response)
520
523
521 # check permissions for this repository
524 # check permissions for this repository
522 perm = self._check_permission(
525 perm = self._check_permission(
523 action, user, self.acl_repo_name, ip_addr,
526 action, user, self.acl_repo_name, ip_addr,
524 plugin, plugin_cache_active, cache_ttl)
527 plugin, plugin_cache_active, cache_ttl)
525 if not perm:
528 if not perm:
526 return HTTPForbidden()(environ, start_response)
529 return HTTPForbidden()(environ, start_response)
527
530
528 # extras are injected into UI object and later available
531 # extras are injected into UI object and later available
529 # in hooks executed by RhodeCode
532 # in hooks executed by RhodeCode
530 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
533 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
531 extras = vcs_operation_context(
534 extras = vcs_operation_context(
532 environ, repo_name=self.acl_repo_name, username=username,
535 environ, repo_name=self.acl_repo_name, username=username,
533 action=action, scm=self.SCM, check_locking=check_locking,
536 action=action, scm=self.SCM, check_locking=check_locking,
534 is_shadow_repo=self.is_shadow_repo
537 is_shadow_repo=self.is_shadow_repo
535 )
538 )
536
539
537 # ======================================================================
540 # ======================================================================
538 # REQUEST HANDLING
541 # REQUEST HANDLING
539 # ======================================================================
542 # ======================================================================
540 repo_path = os.path.join(
543 repo_path = os.path.join(
541 safe_str(self.base_path), safe_str(self.vcs_repo_name))
544 safe_str(self.base_path), safe_str(self.vcs_repo_name))
542 log.debug('Repository path is %s', repo_path)
545 log.debug('Repository path is %s', repo_path)
543
546
544 fix_PATH()
547 fix_PATH()
545
548
546 log.info(
549 log.info(
547 '%s action on %s repo "%s" by "%s" from %s %s',
550 '%s action on %s repo "%s" by "%s" from %s %s',
548 action, self.SCM, safe_str(self.url_repo_name),
551 action, self.SCM, safe_str(self.url_repo_name),
549 safe_str(username), ip_addr, user_agent)
552 safe_str(username), ip_addr, user_agent)
550
553
551 return self._generate_vcs_response(
554 return self._generate_vcs_response(
552 environ, start_response, repo_path, extras, action)
555 environ, start_response, repo_path, extras, action)
553
556
554 @initialize_generator
557 @initialize_generator
555 def _generate_vcs_response(
558 def _generate_vcs_response(
556 self, environ, start_response, repo_path, extras, action):
559 self, environ, start_response, repo_path, extras, action):
557 """
560 """
558 Returns a generator for the response content.
561 Returns a generator for the response content.
559
562
560 This method is implemented as a generator, so that it can trigger
563 This method is implemented as a generator, so that it can trigger
561 the cache validation after all content sent back to the client. It
564 the cache validation after all content sent back to the client. It
562 also handles the locking exceptions which will be triggered when
565 also handles the locking exceptions which will be triggered when
563 the first chunk is produced by the underlying WSGI application.
566 the first chunk is produced by the underlying WSGI application.
564 """
567 """
565 callback_daemon, extras = self._prepare_callback_daemon(extras)
568 callback_daemon, extras = self._prepare_callback_daemon(extras)
566 config = self._create_config(extras, self.acl_repo_name)
569 config = self._create_config(extras, self.acl_repo_name)
567 log.debug('HOOKS extras is %s', extras)
570 log.debug('HOOKS extras is %s', extras)
568 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
571 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
569 app.rc_extras = extras
572 app.rc_extras = extras
570
573
571 try:
574 try:
572 with callback_daemon:
575 with callback_daemon:
573 try:
576 try:
574 response = app(environ, start_response)
577 response = app(environ, start_response)
575 finally:
578 finally:
576 # This statement works together with the decorator
579 # This statement works together with the decorator
577 # "initialize_generator" above. The decorator ensures that
580 # "initialize_generator" above. The decorator ensures that
578 # we hit the first yield statement before the generator is
581 # we hit the first yield statement before the generator is
579 # returned back to the WSGI server. This is needed to
582 # returned back to the WSGI server. This is needed to
580 # ensure that the call to "app" above triggers the
583 # ensure that the call to "app" above triggers the
581 # needed callback to "start_response" before the
584 # needed callback to "start_response" before the
582 # generator is actually used.
585 # generator is actually used.
583 yield "__init__"
586 yield "__init__"
584
587
585 for chunk in response:
588 for chunk in response:
586 yield chunk
589 yield chunk
587 except Exception as exc:
590 except Exception as exc:
588 # TODO: martinb: Exceptions are only raised in case of the Pyro4
591 # TODO: martinb: Exceptions are only raised in case of the Pyro4
589 # backend. Refactor this except block after dropping Pyro4 support.
592 # backend. Refactor this except block after dropping Pyro4 support.
590 # TODO: johbo: Improve "translating" back the exception.
593 # TODO: johbo: Improve "translating" back the exception.
591 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
594 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
592 exc = HTTPLockedRC(*exc.args)
595 exc = HTTPLockedRC(*exc.args)
593 _code = rhodecode.CONFIG.get('lock_ret_code')
596 _code = rhodecode.CONFIG.get('lock_ret_code')
594 log.debug('Repository LOCKED ret code %s!', (_code,))
597 log.debug('Repository LOCKED ret code %s!', (_code,))
595 elif getattr(exc, '_vcs_kind', None) == 'requirement':
598 elif getattr(exc, '_vcs_kind', None) == 'requirement':
596 log.debug(
599 log.debug(
597 'Repository requires features unknown to this Mercurial')
600 'Repository requires features unknown to this Mercurial')
598 exc = HTTPRequirementError(*exc.args)
601 exc = HTTPRequirementError(*exc.args)
599 else:
602 else:
600 raise
603 raise
601
604
602 for chunk in exc(environ, start_response):
605 for chunk in exc(environ, start_response):
603 yield chunk
606 yield chunk
604 finally:
607 finally:
605 # invalidate cache on push
608 # invalidate cache on push
606 try:
609 try:
607 if action == 'push':
610 if action == 'push':
608 self._invalidate_cache(self.url_repo_name)
611 self._invalidate_cache(self.url_repo_name)
609 finally:
612 finally:
610 meta.Session.remove()
613 meta.Session.remove()
611
614
612 def _get_repository_name(self, environ):
615 def _get_repository_name(self, environ):
613 """Get repository name out of the environmnent
616 """Get repository name out of the environmnent
614
617
615 :param environ: WSGI environment
618 :param environ: WSGI environment
616 """
619 """
617 raise NotImplementedError()
620 raise NotImplementedError()
618
621
619 def _get_action(self, environ):
622 def _get_action(self, environ):
620 """Map request commands into a pull or push command.
623 """Map request commands into a pull or push command.
621
624
622 :param environ: WSGI environment
625 :param environ: WSGI environment
623 """
626 """
624 raise NotImplementedError()
627 raise NotImplementedError()
625
628
626 def _create_wsgi_app(self, repo_path, repo_name, config):
629 def _create_wsgi_app(self, repo_path, repo_name, config):
627 """Return the WSGI app that will finally handle the request."""
630 """Return the WSGI app that will finally handle the request."""
628 raise NotImplementedError()
631 raise NotImplementedError()
629
632
630 def _create_config(self, extras, repo_name):
633 def _create_config(self, extras, repo_name):
631 """Create a safe config representation."""
634 """Create a safe config representation."""
632 raise NotImplementedError()
635 raise NotImplementedError()
633
636
634 def _prepare_callback_daemon(self, extras):
637 def _prepare_callback_daemon(self, extras):
635 return prepare_callback_daemon(
638 return prepare_callback_daemon(
636 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
639 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
637 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
640 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
638
641
639
642
640 def _should_check_locking(query_string):
643 def _should_check_locking(query_string):
641 # this is kind of hacky, but due to how mercurial handles client-server
644 # this is kind of hacky, but due to how mercurial handles client-server
642 # server see all operation on commit; bookmarks, phases and
645 # server see all operation on commit; bookmarks, phases and
643 # obsolescence marker in different transaction, we don't want to check
646 # obsolescence marker in different transaction, we don't want to check
644 # locking on those
647 # locking on those
645 return query_string not in ['cmd=listkeys']
648 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now