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