##// END OF EJS Templates
scm-app: allow to set documented pyro4 as scm_app_implementation....
marcink -
r632:064401fe default
parent child Browse files
Show More
@@ -1,445 +1,445 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import logging
28 28 import importlib
29 29 from functools import wraps
30 30
31 31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 32 from webob.exc import (
33 33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 34
35 35 import rhodecode
36 36 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 39 from rhodecode.lib.exceptions import (
40 40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 41 NotAllowedToCreateUserError)
42 42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 43 from rhodecode.lib.middleware import appenlight
44 44 from rhodecode.lib.middleware.utils import scm_app
45 45 from rhodecode.lib.utils import (
46 46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 48 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 49 from rhodecode.model import meta
50 50 from rhodecode.model.db import User, Repository
51 51 from rhodecode.model.scm import ScmModel
52 52 from rhodecode.model.settings import SettingsModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def initialize_generator(factory):
58 58 """
59 59 Initializes the returned generator by draining its first element.
60 60
61 61 This can be used to give a generator an initializer, which is the code
62 62 up to the first yield statement. This decorator enforces that the first
63 63 produced element has the value ``"__init__"`` to make its special
64 64 purpose very explicit in the using code.
65 65 """
66 66
67 67 @wraps(factory)
68 68 def wrapper(*args, **kwargs):
69 69 gen = factory(*args, **kwargs)
70 70 try:
71 71 init = gen.next()
72 72 except StopIteration:
73 73 raise ValueError('Generator must yield at least one element.')
74 74 if init != "__init__":
75 75 raise ValueError('First yielded element must be "__init__".')
76 76 return gen
77 77 return wrapper
78 78
79 79
80 80 class SimpleVCS(object):
81 81 """Common functionality for SCM HTTP handlers."""
82 82
83 83 SCM = 'unknown'
84 84
85 85 def __init__(self, application, config, registry):
86 86 self.registry = registry
87 87 self.application = application
88 88 self.config = config
89 89 # base path of repo locations
90 90 self.basepath = get_rhodecode_base_path()
91 91 # authenticate this VCS request using authfunc
92 92 auth_ret_code_detection = \
93 93 str2bool(self.config.get('auth_ret_code_detection', False))
94 94 self.authenticate = BasicAuth(
95 95 '', authenticate, registry, config.get('auth_ret_code'),
96 96 auth_ret_code_detection)
97 97 self.ip_addr = '0.0.0.0'
98 98
99 99 @property
100 100 def scm_app(self):
101 101 custom_implementation = self.config.get('vcs.scm_app_implementation')
102 if custom_implementation:
102 if custom_implementation and custom_implementation != 'pyro4':
103 103 log.info(
104 104 "Using custom implementation of scm_app: %s",
105 105 custom_implementation)
106 106 scm_app_impl = importlib.import_module(custom_implementation)
107 107 else:
108 108 scm_app_impl = scm_app
109 109 return scm_app_impl
110 110
111 111 def _get_by_id(self, repo_name):
112 112 """
113 113 Gets a special pattern _<ID> from clone url and tries to replace it
114 114 with a repository_name for support of _<ID> non changable urls
115 115
116 116 :param repo_name:
117 117 """
118 118
119 119 data = repo_name.split('/')
120 120 if len(data) >= 2:
121 121 from rhodecode.model.repo import RepoModel
122 122 by_id_match = RepoModel().get_repo_by_id(repo_name)
123 123 if by_id_match:
124 124 data[1] = by_id_match.repo_name
125 125
126 126 return safe_str('/'.join(data))
127 127
128 128 def _invalidate_cache(self, repo_name):
129 129 """
130 130 Set's cache for this repository for invalidation on next access
131 131
132 132 :param repo_name: full repo name, also a cache key
133 133 """
134 134 ScmModel().mark_for_invalidation(repo_name)
135 135
136 136 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
137 137 db_repo = Repository.get_by_repo_name(repo_name)
138 138 if not db_repo:
139 139 log.debug('Repository `%s` not found inside the database.',
140 140 repo_name)
141 141 return False
142 142
143 143 if db_repo.repo_type != scm_type:
144 144 log.warning(
145 145 'Repository `%s` have incorrect scm_type, expected %s got %s',
146 146 repo_name, db_repo.repo_type, scm_type)
147 147 return False
148 148
149 149 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
150 150
151 151 def valid_and_active_user(self, user):
152 152 """
153 153 Checks if that user is not empty, and if it's actually object it checks
154 154 if he's active.
155 155
156 156 :param user: user object or None
157 157 :return: boolean
158 158 """
159 159 if user is None:
160 160 return False
161 161
162 162 elif user.active:
163 163 return True
164 164
165 165 return False
166 166
167 167 def _check_permission(self, action, user, repo_name, ip_addr=None):
168 168 """
169 169 Checks permissions using action (push/pull) user and repository
170 170 name
171 171
172 172 :param action: push or pull action
173 173 :param user: user instance
174 174 :param repo_name: repository name
175 175 """
176 176 # check IP
177 177 inherit = user.inherit_default_permissions
178 178 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
179 179 inherit_from_default=inherit)
180 180 if ip_allowed:
181 181 log.info('Access for IP:%s allowed', ip_addr)
182 182 else:
183 183 return False
184 184
185 185 if action == 'push':
186 186 if not HasPermissionAnyMiddleware('repository.write',
187 187 'repository.admin')(user,
188 188 repo_name):
189 189 return False
190 190
191 191 else:
192 192 # any other action need at least read permission
193 193 if not HasPermissionAnyMiddleware('repository.read',
194 194 'repository.write',
195 195 'repository.admin')(user,
196 196 repo_name):
197 197 return False
198 198
199 199 return True
200 200
201 201 def _check_ssl(self, environ, start_response):
202 202 """
203 203 Checks the SSL check flag and returns False if SSL is not present
204 204 and required True otherwise
205 205 """
206 206 org_proto = environ['wsgi._org_proto']
207 207 # check if we have SSL required ! if not it's a bad request !
208 208 require_ssl = str2bool(
209 209 SettingsModel().get_ui_by_key('push_ssl').ui_value)
210 210 if require_ssl and org_proto == 'http':
211 211 log.debug('proto is %s and SSL is required BAD REQUEST !',
212 212 org_proto)
213 213 return False
214 214 return True
215 215
216 216 def __call__(self, environ, start_response):
217 217 try:
218 218 return self._handle_request(environ, start_response)
219 219 except Exception:
220 220 log.exception("Exception while handling request")
221 221 appenlight.track_exception(environ)
222 222 return HTTPInternalServerError()(environ, start_response)
223 223 finally:
224 224 meta.Session.remove()
225 225
226 226 def _handle_request(self, environ, start_response):
227 227
228 228 if not self._check_ssl(environ, start_response):
229 229 reason = ('SSL required, while RhodeCode was unable '
230 230 'to detect this as SSL request')
231 231 log.debug('User not allowed to proceed, %s', reason)
232 232 return HTTPNotAcceptable(reason)(environ, start_response)
233 233
234 234 ip_addr = get_ip_addr(environ)
235 235 username = None
236 236
237 237 # skip passing error to error controller
238 238 environ['pylons.status_code_redirect'] = True
239 239
240 240 # ======================================================================
241 241 # EXTRACT REPOSITORY NAME FROM ENV
242 242 # ======================================================================
243 243 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
244 244 repo_name = self._get_repository_name(environ)
245 245 environ['REPO_NAME'] = repo_name
246 246 log.debug('Extracted repo name is %s', repo_name)
247 247
248 248 # check for type, presence in database and on filesystem
249 249 if not self.is_valid_and_existing_repo(
250 250 repo_name, self.basepath, self.SCM):
251 251 return HTTPNotFound()(environ, start_response)
252 252
253 253 # ======================================================================
254 254 # GET ACTION PULL or PUSH
255 255 # ======================================================================
256 256 action = self._get_action(environ)
257 257
258 258 # ======================================================================
259 259 # CHECK ANONYMOUS PERMISSION
260 260 # ======================================================================
261 261 if action in ['pull', 'push']:
262 262 anonymous_user = User.get_default_user()
263 263 username = anonymous_user.username
264 264 if anonymous_user.active:
265 265 # ONLY check permissions if the user is activated
266 266 anonymous_perm = self._check_permission(
267 267 action, anonymous_user, repo_name, ip_addr)
268 268 else:
269 269 anonymous_perm = False
270 270
271 271 if not anonymous_user.active or not anonymous_perm:
272 272 if not anonymous_user.active:
273 273 log.debug('Anonymous access is disabled, running '
274 274 'authentication')
275 275
276 276 if not anonymous_perm:
277 277 log.debug('Not enough credentials to access this '
278 278 'repository as anonymous user')
279 279
280 280 username = None
281 281 # ==============================================================
282 282 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
283 283 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
284 284 # ==============================================================
285 285
286 286 # try to auth based on environ, container auth methods
287 287 log.debug('Running PRE-AUTH for container based authentication')
288 288 pre_auth = authenticate(
289 289 '', '', environ, VCS_TYPE, registry=self.registry)
290 290 if pre_auth and pre_auth.get('username'):
291 291 username = pre_auth['username']
292 292 log.debug('PRE-AUTH got %s as username', username)
293 293
294 294 # If not authenticated by the container, running basic auth
295 295 if not username:
296 296 self.authenticate.realm = get_rhodecode_realm()
297 297
298 298 try:
299 299 result = self.authenticate(environ)
300 300 except (UserCreationError, NotAllowedToCreateUserError) as e:
301 301 log.error(e)
302 302 reason = safe_str(e)
303 303 return HTTPNotAcceptable(reason)(environ, start_response)
304 304
305 305 if isinstance(result, str):
306 306 AUTH_TYPE.update(environ, 'basic')
307 307 REMOTE_USER.update(environ, result)
308 308 username = result
309 309 else:
310 310 return result.wsgi_application(environ, start_response)
311 311
312 312 # ==============================================================
313 313 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
314 314 # ==============================================================
315 315 user = User.get_by_username(username)
316 316 if not self.valid_and_active_user(user):
317 317 return HTTPForbidden()(environ, start_response)
318 318 username = user.username
319 319 user.update_lastactivity()
320 320 meta.Session().commit()
321 321
322 322 # check user attributes for password change flag
323 323 user_obj = user
324 324 if user_obj and user_obj.username != User.DEFAULT_USER and \
325 325 user_obj.user_data.get('force_password_change'):
326 326 reason = 'password change required'
327 327 log.debug('User not allowed to authenticate, %s', reason)
328 328 return HTTPNotAcceptable(reason)(environ, start_response)
329 329
330 330 # check permissions for this repository
331 331 perm = self._check_permission(action, user, repo_name, ip_addr)
332 332 if not perm:
333 333 return HTTPForbidden()(environ, start_response)
334 334
335 335 # extras are injected into UI object and later available
336 336 # in hooks executed by rhodecode
337 337 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
338 338 extras = vcs_operation_context(
339 339 environ, repo_name=repo_name, username=username,
340 340 action=action, scm=self.SCM,
341 341 check_locking=check_locking)
342 342
343 343 # ======================================================================
344 344 # REQUEST HANDLING
345 345 # ======================================================================
346 346 str_repo_name = safe_str(repo_name)
347 347 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
348 348 log.debug('Repository path is %s', repo_path)
349 349
350 350 fix_PATH()
351 351
352 352 log.info(
353 353 '%s action on %s repo "%s" by "%s" from %s',
354 354 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
355 355
356 356 return self._generate_vcs_response(
357 357 environ, start_response, repo_path, repo_name, extras, action)
358 358
359 359 @initialize_generator
360 360 def _generate_vcs_response(
361 361 self, environ, start_response, repo_path, repo_name, extras,
362 362 action):
363 363 """
364 364 Returns a generator for the response content.
365 365
366 366 This method is implemented as a generator, so that it can trigger
367 367 the cache validation after all content sent back to the client. It
368 368 also handles the locking exceptions which will be triggered when
369 369 the first chunk is produced by the underlying WSGI application.
370 370 """
371 371 callback_daemon, extras = self._prepare_callback_daemon(extras)
372 372 config = self._create_config(extras, repo_name)
373 373 log.debug('HOOKS extras is %s', extras)
374 374 app = self._create_wsgi_app(repo_path, repo_name, config)
375 375
376 376 try:
377 377 with callback_daemon:
378 378 try:
379 379 response = app(environ, start_response)
380 380 finally:
381 381 # This statement works together with the decorator
382 382 # "initialize_generator" above. The decorator ensures that
383 383 # we hit the first yield statement before the generator is
384 384 # returned back to the WSGI server. This is needed to
385 385 # ensure that the call to "app" above triggers the
386 386 # needed callback to "start_response" before the
387 387 # generator is actually used.
388 388 yield "__init__"
389 389
390 390 for chunk in response:
391 391 yield chunk
392 392 except Exception as exc:
393 393 # TODO: johbo: Improve "translating" back the exception.
394 394 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
395 395 exc = HTTPLockedRC(*exc.args)
396 396 _code = rhodecode.CONFIG.get('lock_ret_code')
397 397 log.debug('Repository LOCKED ret code %s!', (_code,))
398 398 elif getattr(exc, '_vcs_kind', None) == 'requirement':
399 399 log.debug(
400 400 'Repository requires features unknown to this Mercurial')
401 401 exc = HTTPRequirementError(*exc.args)
402 402 else:
403 403 raise
404 404
405 405 for chunk in exc(environ, start_response):
406 406 yield chunk
407 407 finally:
408 408 # invalidate cache on push
409 409 if action == 'push':
410 410 self._invalidate_cache(repo_name)
411 411
412 412 def _get_repository_name(self, environ):
413 413 """Get repository name out of the environmnent
414 414
415 415 :param environ: WSGI environment
416 416 """
417 417 raise NotImplementedError()
418 418
419 419 def _get_action(self, environ):
420 420 """Map request commands into a pull or push command.
421 421
422 422 :param environ: WSGI environment
423 423 """
424 424 raise NotImplementedError()
425 425
426 426 def _create_wsgi_app(self, repo_path, repo_name, config):
427 427 """Return the WSGI app that will finally handle the request."""
428 428 raise NotImplementedError()
429 429
430 430 def _create_config(self, extras, repo_name):
431 431 """Create a Pyro safe config representation."""
432 432 raise NotImplementedError()
433 433
434 434 def _prepare_callback_daemon(self, extras):
435 435 return prepare_callback_daemon(
436 436 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
437 437 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
438 438
439 439
440 440 def _should_check_locking(query_string):
441 441 # this is kind of hacky, but due to how mercurial handles client-server
442 442 # server see all operation on commit; bookmarks, phases and
443 443 # obsolescence marker in different transaction, we don't want to check
444 444 # locking on those
445 445 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now