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