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