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