##// END OF EJS Templates
app: disconect auth plugin loading from authentication registry....
marcink -
r3241:611f6ed0 default
parent child Browse files
Show More
@@ -1,111 +1,99 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 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 import logging
22 22 import importlib
23 23
24 24 from pyramid.authentication import SessionAuthenticationPolicy
25 25
26 26 from rhodecode.authentication.registry import AuthenticationPluginRegistry
27 27 from rhodecode.authentication.routes import root_factory
28 28 from rhodecode.authentication.routes import AuthnRootResource
29 29 from rhodecode.apps._base import ADMIN_PREFIX
30 30 from rhodecode.model.settings import SettingsModel
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34 legacy_plugin_prefix = 'py:'
35 35 plugin_default_auth_ttl = 30
36 36
37 37
38 38 def _import_legacy_plugin(plugin_id):
39 39 module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1]
40 40 module = importlib.import_module(module_name)
41 41 return module.plugin_factory(plugin_id=plugin_id)
42 42
43 43
44 def _discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
44 def discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
45 45 """
46 46 Function that imports the legacy plugins stored in the 'auth_plugins'
47 47 setting in database which are using the specified prefix. Normally 'py:' is
48 48 used for the legacy plugins.
49 49 """
50 50 log.debug('authentication: running legacy plugin discovery for prefix %s',
51 51 legacy_plugin_prefix)
52 52 try:
53 53 auth_plugins = SettingsModel().get_setting_by_name('auth_plugins')
54 54 enabled_plugins = auth_plugins.app_settings_value
55 55 legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)]
56 56 except Exception:
57 57 legacy_plugins = []
58 58
59 59 for plugin_id in legacy_plugins:
60 60 log.debug('Legacy plugin discovered: "%s"', plugin_id)
61 61 try:
62 62 plugin = _import_legacy_plugin(plugin_id)
63 63 config.include(plugin.includeme)
64 64 except Exception as e:
65 65 log.exception(
66 66 'Exception while loading legacy authentication plugin '
67 67 '"{}": {}'.format(plugin_id, e.message))
68 68
69 69
70 70 def includeme(config):
71 71 # Set authentication policy.
72 72 authn_policy = SessionAuthenticationPolicy()
73 73 config.set_authentication_policy(authn_policy)
74 74
75 75 # Create authentication plugin registry and add it to the pyramid registry.
76 76 authn_registry = AuthenticationPluginRegistry(config.get_settings())
77 77 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
78 78 config.registry.registerUtility(authn_registry)
79 79
80 80 # Create authentication traversal root resource.
81 81 authn_root_resource = root_factory()
82 82 config.add_directive('add_authn_resource',
83 83 authn_root_resource.add_authn_resource)
84 84
85 85 # Add the authentication traversal route.
86 86 config.add_route('auth_home',
87 87 ADMIN_PREFIX + '/auth*traverse',
88 88 factory=root_factory)
89 89 # Add the authentication settings root views.
90 90 config.add_view('rhodecode.authentication.views.AuthSettingsView',
91 91 attr='index',
92 92 request_method='GET',
93 93 route_name='auth_home',
94 94 context=AuthnRootResource)
95 95 config.add_view('rhodecode.authentication.views.AuthSettingsView',
96 96 attr='auth_settings',
97 97 request_method='POST',
98 98 route_name='auth_home',
99 99 context=AuthnRootResource)
100
101 # load CE authentication plugins
102 config.include('rhodecode.authentication.plugins.auth_crowd')
103 config.include('rhodecode.authentication.plugins.auth_headers')
104 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
105 config.include('rhodecode.authentication.plugins.auth_ldap')
106 config.include('rhodecode.authentication.plugins.auth_pam')
107 config.include('rhodecode.authentication.plugins.auth_rhodecode')
108 config.include('rhodecode.authentication.plugins.auth_token')
109
110 # Auto discover authentication plugins and include their configuration.
111 _discover_legacy_plugins(config)
@@ -1,588 +1,614 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26 import time
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 import pyramid.events
30 30 from pyramid.wsgi import wsgiapp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.environment import load_pyramid_environment
42 42
43 43 import rhodecode.events
44 44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 45 from rhodecode.lib.request import Request
46 46 from rhodecode.lib.vcs import VCSCommunicationError
47 47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 50 from rhodecode.lib.celerylib.loader import configure_celery
51 51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 56 write_metadata_if_needed, inject_app_settings)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def is_http_error(response):
63 63 # error which should have traceback
64 64 return response.status_code > 499
65 65
66 66
67 def should_load_all():
68 """
69 Returns if all application components should be loaded. In some cases it's
70 desired to skip apps loading for faster shell script execution
71 """
72 return True
73
74
67 75 def make_pyramid_app(global_config, **settings):
68 76 """
69 77 Constructs the WSGI application based on Pyramid.
70 78
71 79 Specials:
72 80
73 81 * The application can also be integrated like a plugin via the call to
74 82 `includeme`. This is accompanied with the other utility functions which
75 83 are called. Changing this should be done with great care to not break
76 84 cases when these fragments are assembled from another place.
77 85
78 86 """
79 87
80 88 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
81 89 # will be replaced by the value of the environment variable "NAME" in this case.
82 90 start_time = time.time()
83 91
84 92 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
85 93
86 94 global_config = _substitute_values(global_config, environ)
87 95 settings = _substitute_values(settings, environ)
88 96
89 97 sanitize_settings_and_apply_defaults(settings)
90 98
91 99 config = Configurator(settings=settings)
92 100
93 101 # Apply compatibility patches
94 102 patches.inspect_getargspec()
95 103
96 104 load_pyramid_environment(global_config, settings)
97 105
98 106 # Static file view comes first
99 107 includeme_first(config)
100 108
101 109 includeme(config)
102 110
103 111 pyramid_app = config.make_wsgi_app()
104 112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
105 113 pyramid_app.config = config
106 114
107 115 config.configure_celery(global_config['__file__'])
108 116 # creating the app uses a connection - return it after we are done
109 117 meta.Session.remove()
110 118 total_time = time.time() - start_time
111 119 log.info('Pyramid app `%s` created and configured in %.2fs',
112 120 pyramid_app.func_name, total_time)
113 121 return pyramid_app
114 122
115 123
116 124 def not_found_view(request):
117 125 """
118 126 This creates the view which should be registered as not-found-view to
119 127 pyramid.
120 128 """
121 129
122 130 if not getattr(request, 'vcs_call', None):
123 131 # handle like regular case with our error_handler
124 132 return error_handler(HTTPNotFound(), request)
125 133
126 134 # handle not found view as a vcs call
127 135 settings = request.registry.settings
128 136 ae_client = getattr(request, 'ae_client', None)
129 137 vcs_app = VCSMiddleware(
130 138 HTTPNotFound(), request.registry, settings,
131 139 appenlight_client=ae_client)
132 140
133 141 return wsgiapp(vcs_app)(None, request)
134 142
135 143
136 144 def error_handler(exception, request):
137 145 import rhodecode
138 146 from rhodecode.lib import helpers
139 147
140 148 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
141 149
142 150 base_response = HTTPInternalServerError()
143 151 # prefer original exception for the response since it may have headers set
144 152 if isinstance(exception, HTTPException):
145 153 base_response = exception
146 154 elif isinstance(exception, VCSCommunicationError):
147 155 base_response = VCSServerUnavailable()
148 156
149 157 if is_http_error(base_response):
150 158 log.exception(
151 159 'error occurred handling this request for path: %s', request.path)
152 160
153 161 error_explanation = base_response.explanation or str(base_response)
154 162 if base_response.status_code == 404:
155 163 error_explanation += " Or you don't have permission to access it."
156 164 c = AttributeDict()
157 165 c.error_message = base_response.status
158 166 c.error_explanation = error_explanation
159 167 c.visual = AttributeDict()
160 168
161 169 c.visual.rhodecode_support_url = (
162 170 request.registry.settings.get('rhodecode_support_url') or
163 171 request.route_url('rhodecode_support')
164 172 )
165 173 c.redirect_time = 0
166 174 c.rhodecode_name = rhodecode_title
167 175 if not c.rhodecode_name:
168 176 c.rhodecode_name = 'Rhodecode'
169 177
170 178 c.causes = []
171 179 if is_http_error(base_response):
172 180 c.causes.append('Server is overloaded.')
173 181 c.causes.append('Server database connection is lost.')
174 182 c.causes.append('Server expected unhandled error.')
175 183
176 184 if hasattr(base_response, 'causes'):
177 185 c.causes = base_response.causes
178 186
179 187 c.messages = helpers.flash.pop_messages(request=request)
180 188
181 189 exc_info = sys.exc_info()
182 190 c.exception_id = id(exc_info)
183 191 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
184 192 or base_response.status_code > 499
185 193 c.exception_id_url = request.route_url(
186 194 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
187 195
188 196 if c.show_exception_id:
189 197 store_exception(c.exception_id, exc_info)
190 198
191 199 response = render_to_response(
192 200 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
193 201 response=base_response)
194 202
195 203 return response
196 204
197 205
198 206 def includeme_first(config):
199 207 # redirect automatic browser favicon.ico requests to correct place
200 208 def favicon_redirect(context, request):
201 209 return HTTPFound(
202 210 request.static_path('rhodecode:public/images/favicon.ico'))
203 211
204 212 config.add_view(favicon_redirect, route_name='favicon')
205 213 config.add_route('favicon', '/favicon.ico')
206 214
207 215 def robots_redirect(context, request):
208 216 return HTTPFound(
209 217 request.static_path('rhodecode:public/robots.txt'))
210 218
211 219 config.add_view(robots_redirect, route_name='robots')
212 220 config.add_route('robots', '/robots.txt')
213 221
214 222 config.add_static_view(
215 223 '_static/deform', 'deform:static')
216 224 config.add_static_view(
217 225 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
218 226
219 227
220 228 def includeme(config):
221 229 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
222 230 settings = config.registry.settings
223 231 config.set_request_factory(Request)
224 232
225 233 # plugin information
226 234 config.registry.rhodecode_plugins = collections.OrderedDict()
227 235
228 236 config.add_directive(
229 237 'register_rhodecode_plugin', register_rhodecode_plugin)
230 238
231 239 config.add_directive('configure_celery', configure_celery)
232 240
233 241 if asbool(settings.get('appenlight', 'false')):
234 242 config.include('appenlight_client.ext.pyramid_tween')
235 243
244 load_all = should_load_all()
245
236 246 # Includes which are required. The application would fail without them.
237 247 config.include('pyramid_mako')
238 248 config.include('pyramid_beaker')
239 249 config.include('rhodecode.lib.rc_cache')
240 250
241 251 config.include('rhodecode.apps._base.navigation')
242 252 config.include('rhodecode.apps._base.subscribers')
243 253 config.include('rhodecode.tweens')
244 254
245 255 config.include('rhodecode.integrations')
246 256 config.include('rhodecode.authentication')
247 257
258 if load_all:
259 from rhodecode.authentication import discover_legacy_plugins
260 # load CE authentication plugins
261 config.include('rhodecode.authentication.plugins.auth_crowd')
262 config.include('rhodecode.authentication.plugins.auth_headers')
263 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
264 config.include('rhodecode.authentication.plugins.auth_ldap')
265 config.include('rhodecode.authentication.plugins.auth_pam')
266 config.include('rhodecode.authentication.plugins.auth_rhodecode')
267 config.include('rhodecode.authentication.plugins.auth_token')
268
269 # Auto discover authentication plugins and include their configuration.
270 discover_legacy_plugins(config)
271
248 272 # apps
249 273 config.include('rhodecode.apps._base')
250 config.include('rhodecode.apps.ops')
251 config.include('rhodecode.apps.admin')
252 config.include('rhodecode.apps.channelstream')
253 config.include('rhodecode.apps.login')
254 config.include('rhodecode.apps.home')
255 config.include('rhodecode.apps.journal')
256 config.include('rhodecode.apps.repository')
257 config.include('rhodecode.apps.repo_group')
258 config.include('rhodecode.apps.user_group')
259 config.include('rhodecode.apps.search')
260 config.include('rhodecode.apps.user_profile')
261 config.include('rhodecode.apps.user_group_profile')
262 config.include('rhodecode.apps.my_account')
263 config.include('rhodecode.apps.svn_support')
264 config.include('rhodecode.apps.ssh_support')
265 config.include('rhodecode.apps.gist')
266 config.include('rhodecode.apps.debug_style')
267 config.include('rhodecode.api')
274
275 if load_all:
276 config.include('rhodecode.apps.ops')
277 config.include('rhodecode.apps.admin')
278 config.include('rhodecode.apps.channelstream')
279 config.include('rhodecode.apps.login')
280 config.include('rhodecode.apps.home')
281 config.include('rhodecode.apps.journal')
282 config.include('rhodecode.apps.repository')
283 config.include('rhodecode.apps.repo_group')
284 config.include('rhodecode.apps.user_group')
285 config.include('rhodecode.apps.search')
286 config.include('rhodecode.apps.user_profile')
287 config.include('rhodecode.apps.user_group_profile')
288 config.include('rhodecode.apps.my_account')
289 config.include('rhodecode.apps.svn_support')
290 config.include('rhodecode.apps.ssh_support')
291 config.include('rhodecode.apps.gist')
292 config.include('rhodecode.apps.debug_style')
293 config.include('rhodecode.api')
268 294
269 295 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
270 296 config.add_translation_dirs('rhodecode:i18n/')
271 297 settings['default_locale_name'] = settings.get('lang', 'en')
272 298
273 299 # Add subscribers.
274 300 config.add_subscriber(inject_app_settings,
275 301 pyramid.events.ApplicationCreated)
276 302 config.add_subscriber(scan_repositories_if_enabled,
277 303 pyramid.events.ApplicationCreated)
278 304 config.add_subscriber(write_metadata_if_needed,
279 305 pyramid.events.ApplicationCreated)
280 306 config.add_subscriber(write_js_routes_if_enabled,
281 307 pyramid.events.ApplicationCreated)
282 308
283 309 # request custom methods
284 310 config.add_request_method(
285 311 'rhodecode.lib.partial_renderer.get_partial_renderer',
286 312 'get_partial_renderer')
287 313
288 314 # Set the authorization policy.
289 315 authz_policy = ACLAuthorizationPolicy()
290 316 config.set_authorization_policy(authz_policy)
291 317
292 318 # Set the default renderer for HTML templates to mako.
293 319 config.add_mako_renderer('.html')
294 320
295 321 config.add_renderer(
296 322 name='json_ext',
297 323 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
298 324
299 325 # include RhodeCode plugins
300 326 includes = aslist(settings.get('rhodecode.includes', []))
301 327 for inc in includes:
302 328 config.include(inc)
303 329
304 330 # custom not found view, if our pyramid app doesn't know how to handle
305 331 # the request pass it to potential VCS handling ap
306 332 config.add_notfound_view(not_found_view)
307 333 if not settings.get('debugtoolbar.enabled', False):
308 334 # disabled debugtoolbar handle all exceptions via the error_handlers
309 335 config.add_view(error_handler, context=Exception)
310 336
311 337 # all errors including 403/404/50X
312 338 config.add_view(error_handler, context=HTTPError)
313 339
314 340
315 341 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
316 342 """
317 343 Apply outer WSGI middlewares around the application.
318 344 """
319 345 registry = config.registry
320 346 settings = registry.settings
321 347
322 348 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
323 349 pyramid_app = HttpsFixup(pyramid_app, settings)
324 350
325 351 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
326 352 pyramid_app, settings)
327 353 registry.ae_client = _ae_client
328 354
329 355 if settings['gzip_responses']:
330 356 pyramid_app = make_gzip_middleware(
331 357 pyramid_app, settings, compress_level=1)
332 358
333 359 # this should be the outer most middleware in the wsgi stack since
334 360 # middleware like Routes make database calls
335 361 def pyramid_app_with_cleanup(environ, start_response):
336 362 try:
337 363 return pyramid_app(environ, start_response)
338 364 finally:
339 365 # Dispose current database session and rollback uncommitted
340 366 # transactions.
341 367 meta.Session.remove()
342 368
343 369 # In a single threaded mode server, on non sqlite db we should have
344 370 # '0 Current Checked out connections' at the end of a request,
345 371 # if not, then something, somewhere is leaving a connection open
346 372 pool = meta.Base.metadata.bind.engine.pool
347 373 log.debug('sa pool status: %s', pool.status())
348 374 log.debug('Request processing finalized')
349 375
350 376 return pyramid_app_with_cleanup
351 377
352 378
353 379 def sanitize_settings_and_apply_defaults(settings):
354 380 """
355 381 Applies settings defaults and does all type conversion.
356 382
357 383 We would move all settings parsing and preparation into this place, so that
358 384 we have only one place left which deals with this part. The remaining parts
359 385 of the application would start to rely fully on well prepared settings.
360 386
361 387 This piece would later be split up per topic to avoid a big fat monster
362 388 function.
363 389 """
364 390
365 391 settings.setdefault('rhodecode.edition', 'Community Edition')
366 392
367 393 if 'mako.default_filters' not in settings:
368 394 # set custom default filters if we don't have it defined
369 395 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
370 396 settings['mako.default_filters'] = 'h_filter'
371 397
372 398 if 'mako.directories' not in settings:
373 399 mako_directories = settings.setdefault('mako.directories', [
374 400 # Base templates of the original application
375 401 'rhodecode:templates',
376 402 ])
377 403 log.debug(
378 404 "Using the following Mako template directories: %s",
379 405 mako_directories)
380 406
381 407 # Default includes, possible to change as a user
382 408 pyramid_includes = settings.setdefault('pyramid.includes', [
383 409 'rhodecode.lib.middleware.request_wrapper',
384 410 ])
385 411 log.debug(
386 412 "Using the following pyramid.includes: %s",
387 413 pyramid_includes)
388 414
389 415 # TODO: johbo: Re-think this, usually the call to config.include
390 416 # should allow to pass in a prefix.
391 417 settings.setdefault('rhodecode.api.url', '/_admin/api')
392 418
393 419 # Sanitize generic settings.
394 420 _list_setting(settings, 'default_encoding', 'UTF-8')
395 421 _bool_setting(settings, 'is_test', 'false')
396 422 _bool_setting(settings, 'gzip_responses', 'false')
397 423
398 424 # Call split out functions that sanitize settings for each topic.
399 425 _sanitize_appenlight_settings(settings)
400 426 _sanitize_vcs_settings(settings)
401 427 _sanitize_cache_settings(settings)
402 428
403 429 # configure instance id
404 430 config_utils.set_instance_id(settings)
405 431
406 432 return settings
407 433
408 434
409 435 def _sanitize_appenlight_settings(settings):
410 436 _bool_setting(settings, 'appenlight', 'false')
411 437
412 438
413 439 def _sanitize_vcs_settings(settings):
414 440 """
415 441 Applies settings defaults and does type conversion for all VCS related
416 442 settings.
417 443 """
418 444 _string_setting(settings, 'vcs.svn.compatible_version', '')
419 445 _string_setting(settings, 'git_rev_filter', '--all')
420 446 _string_setting(settings, 'vcs.hooks.protocol', 'http')
421 447 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
422 448 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
423 449 _string_setting(settings, 'vcs.server', '')
424 450 _string_setting(settings, 'vcs.server.log_level', 'debug')
425 451 _string_setting(settings, 'vcs.server.protocol', 'http')
426 452 _bool_setting(settings, 'startup.import_repos', 'false')
427 453 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
428 454 _bool_setting(settings, 'vcs.server.enable', 'true')
429 455 _bool_setting(settings, 'vcs.start_server', 'false')
430 456 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
431 457 _int_setting(settings, 'vcs.connection_timeout', 3600)
432 458
433 459 # Support legacy values of vcs.scm_app_implementation. Legacy
434 460 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
435 461 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
436 462 scm_app_impl = settings['vcs.scm_app_implementation']
437 463 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
438 464 settings['vcs.scm_app_implementation'] = 'http'
439 465
440 466
441 467 def _sanitize_cache_settings(settings):
442 468 temp_store = tempfile.gettempdir()
443 469 default_cache_dir = os.path.join(temp_store, 'rc_cache')
444 470
445 471 # save default, cache dir, and use it for all backends later.
446 472 default_cache_dir = _string_setting(
447 473 settings,
448 474 'cache_dir',
449 475 default_cache_dir, lower=False, default_when_empty=True)
450 476
451 477 # ensure we have our dir created
452 478 if not os.path.isdir(default_cache_dir):
453 479 os.makedirs(default_cache_dir, mode=0755)
454 480
455 481 # exception store cache
456 482 _string_setting(
457 483 settings,
458 484 'exception_tracker.store_path',
459 485 temp_store, lower=False, default_when_empty=True)
460 486
461 487 # cache_perms
462 488 _string_setting(
463 489 settings,
464 490 'rc_cache.cache_perms.backend',
465 491 'dogpile.cache.rc.file_namespace', lower=False)
466 492 _int_setting(
467 493 settings,
468 494 'rc_cache.cache_perms.expiration_time',
469 495 60)
470 496 _string_setting(
471 497 settings,
472 498 'rc_cache.cache_perms.arguments.filename',
473 499 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
474 500
475 501 # cache_repo
476 502 _string_setting(
477 503 settings,
478 504 'rc_cache.cache_repo.backend',
479 505 'dogpile.cache.rc.file_namespace', lower=False)
480 506 _int_setting(
481 507 settings,
482 508 'rc_cache.cache_repo.expiration_time',
483 509 60)
484 510 _string_setting(
485 511 settings,
486 512 'rc_cache.cache_repo.arguments.filename',
487 513 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
488 514
489 515 # cache_license
490 516 _string_setting(
491 517 settings,
492 518 'rc_cache.cache_license.backend',
493 519 'dogpile.cache.rc.file_namespace', lower=False)
494 520 _int_setting(
495 521 settings,
496 522 'rc_cache.cache_license.expiration_time',
497 523 5*60)
498 524 _string_setting(
499 525 settings,
500 526 'rc_cache.cache_license.arguments.filename',
501 527 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
502 528
503 529 # cache_repo_longterm memory, 96H
504 530 _string_setting(
505 531 settings,
506 532 'rc_cache.cache_repo_longterm.backend',
507 533 'dogpile.cache.rc.memory_lru', lower=False)
508 534 _int_setting(
509 535 settings,
510 536 'rc_cache.cache_repo_longterm.expiration_time',
511 537 345600)
512 538 _int_setting(
513 539 settings,
514 540 'rc_cache.cache_repo_longterm.max_size',
515 541 10000)
516 542
517 543 # sql_cache_short
518 544 _string_setting(
519 545 settings,
520 546 'rc_cache.sql_cache_short.backend',
521 547 'dogpile.cache.rc.memory_lru', lower=False)
522 548 _int_setting(
523 549 settings,
524 550 'rc_cache.sql_cache_short.expiration_time',
525 551 30)
526 552 _int_setting(
527 553 settings,
528 554 'rc_cache.sql_cache_short.max_size',
529 555 10000)
530 556
531 557
532 558 def _int_setting(settings, name, default):
533 559 settings[name] = int(settings.get(name, default))
534 560 return settings[name]
535 561
536 562
537 563 def _bool_setting(settings, name, default):
538 564 input_val = settings.get(name, default)
539 565 if isinstance(input_val, unicode):
540 566 input_val = input_val.encode('utf8')
541 567 settings[name] = asbool(input_val)
542 568 return settings[name]
543 569
544 570
545 571 def _list_setting(settings, name, default):
546 572 raw_value = settings.get(name, default)
547 573
548 574 old_separator = ','
549 575 if old_separator in raw_value:
550 576 # If we get a comma separated list, pass it to our own function.
551 577 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
552 578 else:
553 579 # Otherwise we assume it uses pyramids space/newline separation.
554 580 settings[name] = aslist(raw_value)
555 581 return settings[name]
556 582
557 583
558 584 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
559 585 value = settings.get(name, default)
560 586
561 587 if default_when_empty and not value:
562 588 # use default value when value is empty
563 589 value = default
564 590
565 591 if lower:
566 592 value = value.lower()
567 593 settings[name] = value
568 594 return settings[name]
569 595
570 596
571 597 def _substitute_values(mapping, substitutions):
572 598
573 599 try:
574 600 result = {
575 601 # Note: Cannot use regular replacements, since they would clash
576 602 # with the implementation of ConfigParser. Using "format" instead.
577 603 key: value.format(**substitutions)
578 604 for key, value in mapping.items()
579 605 }
580 606 except KeyError as e:
581 607 raise ValueError(
582 608 'Failed to substitute env variable: {}. '
583 609 'Make sure you have specified this env variable without ENV_ prefix'.format(e))
584 610 except ValueError as e:
585 611 log.warning('Failed to substitute ENV variable: %s', e)
586 612 result = mapping
587 613
588 614 return result
@@ -1,473 +1,475 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 import base64
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.lib.utils2 import AttributeDict
27 27 from rhodecode.tests.utils import CustomTestApp
28 28
29 29 from rhodecode.lib.caching_query import FromCache
30 30 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
31 31 from rhodecode.lib.middleware import simplevcs
32 32 from rhodecode.lib.middleware.https_fixup import HttpsFixup
33 33 from rhodecode.lib.middleware.utils import scm_app_http
34 34 from rhodecode.model.db import User, _hash_key
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.tests import (
37 37 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
38 38 from rhodecode.tests.lib.middleware import mock_scm_app
39 39
40 40
41 41 class StubVCSController(simplevcs.SimpleVCS):
42 42
43 43 SCM = 'hg'
44 44 stub_response_body = tuple()
45 45
46 46 def __init__(self, *args, **kwargs):
47 47 super(StubVCSController, self).__init__(*args, **kwargs)
48 48 self._action = 'pull'
49 49 self._is_shadow_repo_dir = True
50 50 self._name = HG_REPO
51 51 self.set_repo_names(None)
52 52
53 53 @property
54 54 def is_shadow_repo_dir(self):
55 55 return self._is_shadow_repo_dir
56 56
57 57 def _get_repository_name(self, environ):
58 58 return self._name
59 59
60 60 def _get_action(self, environ):
61 61 return self._action
62 62
63 63 def _create_wsgi_app(self, repo_path, repo_name, config):
64 64 def fake_app(environ, start_response):
65 65 headers = [
66 66 ('Http-Accept', 'application/mercurial')
67 67 ]
68 68 start_response('200 OK', headers)
69 69 return self.stub_response_body
70 70 return fake_app
71 71
72 72 def _create_config(self, extras, repo_name):
73 73 return None
74 74
75 75
76 76 @pytest.fixture
77 77 def vcscontroller(baseapp, config_stub, request_stub):
78 78 config_stub.testing_securitypolicy()
79 79 config_stub.include('rhodecode.authentication')
80 config_stub.include('rhodecode.authentication.plugins.auth_rhodecode')
81 config_stub.include('rhodecode.authentication.plugins.auth_token')
80 82
81 83 controller = StubVCSController(
82 84 baseapp.config.get_settings(), request_stub.registry)
83 85 app = HttpsFixup(controller, baseapp.config.get_settings())
84 86 app = CustomTestApp(app)
85 87
86 88 _remove_default_user_from_query_cache()
87 89
88 90 # Sanity checks that things are set up correctly
89 91 app.get('/' + HG_REPO, status=200)
90 92
91 93 app.controller = controller
92 94 return app
93 95
94 96
95 97 def _remove_default_user_from_query_cache():
96 98 user = User.get_default_user(cache=True)
97 99 query = Session().query(User).filter(User.username == user.username)
98 100 query = query.options(
99 101 FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username)))
100 102 query.invalidate()
101 103 Session().expire(user)
102 104
103 105
104 106 def test_handles_exceptions_during_permissions_checks(
105 107 vcscontroller, disable_anonymous_user):
106 108 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
107 109 auth_password = base64.encodestring(user_and_pass).strip()
108 110 extra_environ = {
109 111 'AUTH_TYPE': 'Basic',
110 112 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
111 113 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
112 114 }
113 115
114 116 # Verify that things are hooked up correctly
115 117 vcscontroller.get('/', status=200, extra_environ=extra_environ)
116 118
117 119 # Simulate trouble during permission checks
118 120 with mock.patch('rhodecode.model.db.User.get_by_username',
119 121 side_effect=Exception) as get_user:
120 122 # Verify that a correct 500 is returned and check that the expected
121 123 # code path was hit.
122 124 vcscontroller.get('/', status=500, extra_environ=extra_environ)
123 125 assert get_user.called
124 126
125 127
126 128 def test_returns_forbidden_if_no_anonymous_access(
127 129 vcscontroller, disable_anonymous_user):
128 130 vcscontroller.get('/', status=401)
129 131
130 132
131 133 class StubFailVCSController(simplevcs.SimpleVCS):
132 134 def _handle_request(self, environ, start_response):
133 135 raise Exception("BOOM")
134 136
135 137
136 138 @pytest.fixture(scope='module')
137 139 def fail_controller(baseapp):
138 140 controller = StubFailVCSController(
139 141 baseapp.config.get_settings(), baseapp.config)
140 142 controller = HttpsFixup(controller, baseapp.config.get_settings())
141 143 controller = CustomTestApp(controller)
142 144 return controller
143 145
144 146
145 147 def test_handles_exceptions_as_internal_server_error(fail_controller):
146 148 fail_controller.get('/', status=500)
147 149
148 150
149 151 def test_provides_traceback_for_appenlight(fail_controller):
150 152 response = fail_controller.get(
151 153 '/', status=500, extra_environ={'appenlight.client': 'fake'})
152 154 assert 'appenlight.__traceback' in response.request.environ
153 155
154 156
155 157 def test_provides_utils_scm_app_as_scm_app_by_default(baseapp, request_stub):
156 158 controller = StubVCSController(baseapp.config.get_settings(), request_stub.registry)
157 159 assert controller.scm_app is scm_app_http
158 160
159 161
160 162 def test_allows_to_override_scm_app_via_config(baseapp, request_stub):
161 163 config = baseapp.config.get_settings().copy()
162 164 config['vcs.scm_app_implementation'] = (
163 165 'rhodecode.tests.lib.middleware.mock_scm_app')
164 166 controller = StubVCSController(config, request_stub.registry)
165 167 assert controller.scm_app is mock_scm_app
166 168
167 169
168 170 @pytest.mark.parametrize('query_string, expected', [
169 171 ('cmd=stub_command', True),
170 172 ('cmd=listkeys', False),
171 173 ])
172 174 def test_should_check_locking(query_string, expected):
173 175 result = simplevcs._should_check_locking(query_string)
174 176 assert result == expected
175 177
176 178
177 179 class TestShadowRepoRegularExpression(object):
178 180 pr_segment = 'pull-request'
179 181 shadow_segment = 'repository'
180 182
181 183 @pytest.mark.parametrize('url, expected', [
182 184 # repo with/without groups
183 185 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
184 186 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
185 187 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
186 188 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
187 189
188 190 # pull request ID
189 191 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
190 192 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
191 193 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
192 194 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
193 195
194 196 # unicode
195 197 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
196 198 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
197 199
198 200 # trailing/leading slash
199 201 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
200 202 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
201 203 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
202 204
203 205 # misc
204 206 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
205 207 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
206 208 ])
207 209 def test_shadow_repo_regular_expression(self, url, expected):
208 210 from rhodecode.lib.middleware.simplevcs import SimpleVCS
209 211 url = url.format(
210 212 pr_segment=self.pr_segment,
211 213 shadow_segment=self.shadow_segment)
212 214 match_obj = SimpleVCS.shadow_repo_re.match(url)
213 215 assert (match_obj is not None) == expected
214 216
215 217
216 218 @pytest.mark.backends('git', 'hg')
217 219 class TestShadowRepoExposure(object):
218 220
219 221 def test_pull_on_shadow_repo_propagates_to_wsgi_app(
220 222 self, baseapp, request_stub):
221 223 """
222 224 Check that a pull action to a shadow repo is propagated to the
223 225 underlying wsgi app.
224 226 """
225 227 controller = StubVCSController(
226 228 baseapp.config.get_settings(), request_stub.registry)
227 229 controller._check_ssl = mock.Mock()
228 230 controller.is_shadow_repo = True
229 231 controller._action = 'pull'
230 232 controller._is_shadow_repo_dir = True
231 233 controller.stub_response_body = 'dummy body value'
232 234 controller._get_default_cache_ttl = mock.Mock(
233 235 return_value=(False, 0))
234 236
235 237 environ_stub = {
236 238 'HTTP_HOST': 'test.example.com',
237 239 'HTTP_ACCEPT': 'application/mercurial',
238 240 'REQUEST_METHOD': 'GET',
239 241 'wsgi.url_scheme': 'http',
240 242 }
241 243
242 244 response = controller(environ_stub, mock.Mock())
243 245 response_body = ''.join(response)
244 246
245 247 # Assert that we got the response from the wsgi app.
246 248 assert response_body == controller.stub_response_body
247 249
248 250 def test_pull_on_shadow_repo_that_is_missing(self, baseapp, request_stub):
249 251 """
250 252 Check that a pull action to a shadow repo is propagated to the
251 253 underlying wsgi app.
252 254 """
253 255 controller = StubVCSController(
254 256 baseapp.config.get_settings(), request_stub.registry)
255 257 controller._check_ssl = mock.Mock()
256 258 controller.is_shadow_repo = True
257 259 controller._action = 'pull'
258 260 controller._is_shadow_repo_dir = False
259 261 controller.stub_response_body = 'dummy body value'
260 262 environ_stub = {
261 263 'HTTP_HOST': 'test.example.com',
262 264 'HTTP_ACCEPT': 'application/mercurial',
263 265 'REQUEST_METHOD': 'GET',
264 266 'wsgi.url_scheme': 'http',
265 267 }
266 268
267 269 response = controller(environ_stub, mock.Mock())
268 270 response_body = ''.join(response)
269 271
270 272 # Assert that we got the response from the wsgi app.
271 273 assert '404 Not Found' in response_body
272 274
273 275 def test_push_on_shadow_repo_raises(self, baseapp, request_stub):
274 276 """
275 277 Check that a push action to a shadow repo is aborted.
276 278 """
277 279 controller = StubVCSController(
278 280 baseapp.config.get_settings(), request_stub.registry)
279 281 controller._check_ssl = mock.Mock()
280 282 controller.is_shadow_repo = True
281 283 controller._action = 'push'
282 284 controller.stub_response_body = 'dummy body value'
283 285 environ_stub = {
284 286 'HTTP_HOST': 'test.example.com',
285 287 'HTTP_ACCEPT': 'application/mercurial',
286 288 'REQUEST_METHOD': 'GET',
287 289 'wsgi.url_scheme': 'http',
288 290 }
289 291
290 292 response = controller(environ_stub, mock.Mock())
291 293 response_body = ''.join(response)
292 294
293 295 assert response_body != controller.stub_response_body
294 296 # Assert that a 406 error is returned.
295 297 assert '406 Not Acceptable' in response_body
296 298
297 299 def test_set_repo_names_no_shadow(self, baseapp, request_stub):
298 300 """
299 301 Check that the set_repo_names method sets all names to the one returned
300 302 by the _get_repository_name method on a request to a non shadow repo.
301 303 """
302 304 environ_stub = {}
303 305 controller = StubVCSController(
304 306 baseapp.config.get_settings(), request_stub.registry)
305 307 controller._name = 'RepoGroup/MyRepo'
306 308 controller.set_repo_names(environ_stub)
307 309 assert not controller.is_shadow_repo
308 310 assert (controller.url_repo_name ==
309 311 controller.acl_repo_name ==
310 312 controller.vcs_repo_name ==
311 313 controller._get_repository_name(environ_stub))
312 314
313 315 def test_set_repo_names_with_shadow(
314 316 self, baseapp, pr_util, config_stub, request_stub):
315 317 """
316 318 Check that the set_repo_names method sets correct names on a request
317 319 to a shadow repo.
318 320 """
319 321 from rhodecode.model.pull_request import PullRequestModel
320 322
321 323 pull_request = pr_util.create_pull_request()
322 324 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
323 325 target=pull_request.target_repo.repo_name,
324 326 pr_id=pull_request.pull_request_id,
325 327 pr_segment=TestShadowRepoRegularExpression.pr_segment,
326 328 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
327 329 controller = StubVCSController(
328 330 baseapp.config.get_settings(), request_stub.registry)
329 331 controller._name = shadow_url
330 332 controller.set_repo_names({})
331 333
332 334 # Get file system path to shadow repo for assertions.
333 335 workspace_id = PullRequestModel()._workspace_id(pull_request)
334 336 target_vcs = pull_request.target_repo.scm_instance()
335 337 vcs_repo_name = target_vcs._get_shadow_repository_path(
336 338 pull_request.target_repo.repo_id, workspace_id)
337 339
338 340 assert controller.vcs_repo_name == vcs_repo_name
339 341 assert controller.url_repo_name == shadow_url
340 342 assert controller.acl_repo_name == pull_request.target_repo.repo_name
341 343 assert controller.is_shadow_repo
342 344
343 345 def test_set_repo_names_with_shadow_but_missing_pr(
344 346 self, baseapp, pr_util, config_stub, request_stub):
345 347 """
346 348 Checks that the set_repo_names method enforces matching target repos
347 349 and pull request IDs.
348 350 """
349 351 pull_request = pr_util.create_pull_request()
350 352 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
351 353 target=pull_request.target_repo.repo_name,
352 354 pr_id=999999999,
353 355 pr_segment=TestShadowRepoRegularExpression.pr_segment,
354 356 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
355 357 controller = StubVCSController(
356 358 baseapp.config.get_settings(), request_stub.registry)
357 359 controller._name = shadow_url
358 360 controller.set_repo_names({})
359 361
360 362 assert not controller.is_shadow_repo
361 363 assert (controller.url_repo_name ==
362 364 controller.acl_repo_name ==
363 365 controller.vcs_repo_name)
364 366
365 367
366 368 @pytest.mark.usefixtures('baseapp')
367 369 class TestGenerateVcsResponse(object):
368 370
369 371 def test_ensures_that_start_response_is_called_early_enough(self):
370 372 self.call_controller_with_response_body(iter(['a', 'b']))
371 373 assert self.start_response.called
372 374
373 375 def test_invalidates_cache_after_body_is_consumed(self):
374 376 result = self.call_controller_with_response_body(iter(['a', 'b']))
375 377 assert not self.was_cache_invalidated()
376 378 # Consume the result
377 379 list(result)
378 380 assert self.was_cache_invalidated()
379 381
380 382 def test_raises_unknown_exceptions(self):
381 383 result = self.call_controller_with_response_body(
382 384 self.raise_result_iter(vcs_kind='unknown'))
383 385 with pytest.raises(Exception):
384 386 list(result)
385 387
386 388 def test_prepare_callback_daemon_is_called(self):
387 389 def side_effect(extras, environ, action, txn_id=None):
388 390 return DummyHooksCallbackDaemon(), extras
389 391
390 392 prepare_patcher = mock.patch.object(
391 393 StubVCSController, '_prepare_callback_daemon')
392 394 with prepare_patcher as prepare_mock:
393 395 prepare_mock.side_effect = side_effect
394 396 self.call_controller_with_response_body(iter(['a', 'b']))
395 397 assert prepare_mock.called
396 398 assert prepare_mock.call_count == 1
397 399
398 400 def call_controller_with_response_body(self, response_body):
399 401 settings = {
400 402 'base_path': 'fake_base_path',
401 403 'vcs.hooks.protocol': 'http',
402 404 'vcs.hooks.direct_calls': False,
403 405 }
404 406 registry = AttributeDict()
405 407 controller = StubVCSController(settings, registry)
406 408 controller._invalidate_cache = mock.Mock()
407 409 controller.stub_response_body = response_body
408 410 self.start_response = mock.Mock()
409 411 result = controller._generate_vcs_response(
410 412 environ={}, start_response=self.start_response,
411 413 repo_path='fake_repo_path',
412 414 extras={}, action='push')
413 415 self.controller = controller
414 416 return result
415 417
416 418 def raise_result_iter(self, vcs_kind='repo_locked'):
417 419 """
418 420 Simulates an exception due to a vcs raised exception if kind vcs_kind
419 421 """
420 422 raise self.vcs_exception(vcs_kind=vcs_kind)
421 423 yield "never_reached"
422 424
423 425 def vcs_exception(self, vcs_kind='repo_locked'):
424 426 locked_exception = Exception('TEST_MESSAGE')
425 427 locked_exception._vcs_kind = vcs_kind
426 428 return locked_exception
427 429
428 430 def was_cache_invalidated(self):
429 431 return self.controller._invalidate_cache.called
430 432
431 433
432 434 class TestInitializeGenerator(object):
433 435
434 436 def test_drains_first_element(self):
435 437 gen = self.factory(['__init__', 1, 2])
436 438 result = list(gen)
437 439 assert result == [1, 2]
438 440
439 441 @pytest.mark.parametrize('values', [
440 442 [],
441 443 [1, 2],
442 444 ])
443 445 def test_raises_value_error(self, values):
444 446 with pytest.raises(ValueError):
445 447 self.factory(values)
446 448
447 449 @simplevcs.initialize_generator
448 450 def factory(self, iterable):
449 451 for elem in iterable:
450 452 yield elem
451 453
452 454
453 455 class TestPrepareHooksDaemon(object):
454 456 def test_calls_imported_prepare_callback_daemon(self, app_settings, request_stub):
455 457 expected_extras = {'extra1': 'value1'}
456 458 daemon = DummyHooksCallbackDaemon()
457 459
458 460 controller = StubVCSController(app_settings, request_stub.registry)
459 461 prepare_patcher = mock.patch.object(
460 462 simplevcs, 'prepare_callback_daemon',
461 463 return_value=(daemon, expected_extras))
462 464 with prepare_patcher as prepare_mock:
463 465 callback_daemon, extras = controller._prepare_callback_daemon(
464 466 expected_extras.copy(), {}, 'push')
465 467 prepare_mock.assert_called_once_with(
466 468 expected_extras,
467 469 protocol=app_settings['vcs.hooks.protocol'],
468 470 host=app_settings['vcs.hooks.host'],
469 471 txn_id=None,
470 472 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
471 473
472 474 assert callback_daemon == daemon
473 475 assert extras == extras
@@ -1,294 +1,296 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 import formencode
23 23 import pytest
24 24
25 25 from rhodecode.tests import (
26 26 HG_REPO, TEST_USER_REGULAR2_EMAIL, TEST_USER_REGULAR2_LOGIN,
27 27 TEST_USER_REGULAR2_PASS, TEST_USER_ADMIN_LOGIN, TESTS_TMP_PATH)
28 28
29 29 from rhodecode.model import validators as v
30 30 from rhodecode.model.user_group import UserGroupModel
31 31
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.repo_group import RepoGroupModel
34 34 from rhodecode.model.db import ChangesetStatus, Repository
35 35 from rhodecode.model.changeset_status import ChangesetStatusModel
36 36 from rhodecode.tests.fixture import Fixture
37 37
38 38 fixture = Fixture()
39 39
40 40 pytestmark = pytest.mark.usefixtures('baseapp')
41 41
42 42
43 43 @pytest.fixture
44 44 def localizer():
45 45 def func(msg):
46 46 return msg
47 47 return func
48 48
49 49
50 50 def test_Message_extractor(localizer):
51 51 validator = v.ValidUsername(localizer)
52 52 pytest.raises(formencode.Invalid, validator.to_python, 'default')
53 53
54 54 class StateObj(object):
55 55 pass
56 56
57 57 pytest.raises(
58 58 formencode.Invalid, validator.to_python, 'default', StateObj)
59 59
60 60
61 61 def test_ValidUsername(localizer):
62 62 validator = v.ValidUsername(localizer)
63 63
64 64 pytest.raises(formencode.Invalid, validator.to_python, 'default')
65 65 pytest.raises(formencode.Invalid, validator.to_python, 'new_user')
66 66 pytest.raises(formencode.Invalid, validator.to_python, '.,')
67 67 pytest.raises(
68 68 formencode.Invalid, validator.to_python, TEST_USER_ADMIN_LOGIN)
69 69 assert 'test' == validator.to_python('test')
70 70
71 71 validator = v.ValidUsername(localizer, edit=True, old_data={'user_id': 1})
72 72
73 73
74 74 def test_ValidRepoUser(localizer):
75 75 validator = v.ValidRepoUser(localizer)
76 76 pytest.raises(formencode.Invalid, validator.to_python, 'nouser')
77 77 assert TEST_USER_ADMIN_LOGIN == \
78 78 validator.to_python(TEST_USER_ADMIN_LOGIN)
79 79
80 80
81 81 def test_ValidUserGroup(localizer):
82 82 validator = v.ValidUserGroup(localizer)
83 83 pytest.raises(formencode.Invalid, validator.to_python, 'default')
84 84 pytest.raises(formencode.Invalid, validator.to_python, '.,')
85 85
86 86 gr = fixture.create_user_group('test')
87 87 gr2 = fixture.create_user_group('tes2')
88 88 Session().commit()
89 89 pytest.raises(formencode.Invalid, validator.to_python, 'test')
90 90 assert gr.users_group_id is not None
91 91 validator = v.ValidUserGroup(localizer,
92 92 edit=True,
93 93 old_data={'users_group_id': gr2.users_group_id})
94 94
95 95 pytest.raises(formencode.Invalid, validator.to_python, 'test')
96 96 pytest.raises(formencode.Invalid, validator.to_python, 'TesT')
97 97 pytest.raises(formencode.Invalid, validator.to_python, 'TEST')
98 98 UserGroupModel().delete(gr)
99 99 UserGroupModel().delete(gr2)
100 100 Session().commit()
101 101
102 102
103 103 @pytest.fixture(scope='function')
104 104 def repo_group(request):
105 105 model = RepoGroupModel()
106 106 gr = model.create(
107 107 group_name='test_gr', group_description='desc', just_db=True,
108 108 owner=TEST_USER_ADMIN_LOGIN)
109 109
110 110 def cleanup():
111 111 model.delete(gr)
112 112
113 113 request.addfinalizer(cleanup)
114 114
115 115 return gr
116 116
117 117
118 118 def test_ValidRepoGroup_same_name_as_repo(localizer):
119 119 validator = v.ValidRepoGroup(localizer)
120 120 with pytest.raises(formencode.Invalid) as excinfo:
121 121 validator.to_python({'group_name': HG_REPO})
122 122 expected_msg = 'Repository with name "vcs_test_hg" already exists'
123 123 assert expected_msg in str(excinfo.value)
124 124
125 125
126 126 def test_ValidRepoGroup_group_exists(localizer, repo_group):
127 127 validator = v.ValidRepoGroup(localizer)
128 128 with pytest.raises(formencode.Invalid) as excinfo:
129 129 validator.to_python({'group_name': repo_group.group_name})
130 130 expected_msg = 'Group "test_gr" already exists'
131 131 assert expected_msg in str(excinfo.value)
132 132
133 133
134 134 def test_ValidRepoGroup_invalid_parent(localizer, repo_group):
135 135 validator = v.ValidRepoGroup(localizer, edit=True,
136 136 old_data={'group_id': repo_group.group_id})
137 137 with pytest.raises(formencode.Invalid) as excinfo:
138 138 validator.to_python({
139 139 'group_name': repo_group.group_name + 'n',
140 140 'group_parent_id': repo_group.group_id,
141 141 })
142 142 expected_msg = 'Cannot assign this group as parent'
143 143 assert expected_msg in str(excinfo.value)
144 144
145 145
146 146 def test_ValidRepoGroup_edit_group_no_root_permission(localizer, repo_group):
147 147 validator = v.ValidRepoGroup(localizer,
148 148 edit=True, old_data={'group_id': repo_group.group_id},
149 149 can_create_in_root=False)
150 150
151 151 # Cannot change parent
152 152 with pytest.raises(formencode.Invalid) as excinfo:
153 153 validator.to_python({'group_parent_id': '25'})
154 154 expected_msg = 'no permission to store repository group in root location'
155 155 assert expected_msg in str(excinfo.value)
156 156
157 157 # Chaning all the other fields is allowed
158 158 validator.to_python({'group_name': 'foo', 'group_parent_id': '-1'})
159 159 validator.to_python(
160 160 {'user': TEST_USER_REGULAR2_LOGIN, 'group_parent_id': '-1'})
161 161 validator.to_python({'group_description': 'bar', 'group_parent_id': '-1'})
162 162 validator.to_python({'enable_locking': 'true', 'group_parent_id': '-1'})
163 163
164 164
165 165 def test_ValidPassword(localizer):
166 166 validator = v.ValidPassword(localizer)
167 167 assert 'lol' == validator.to_python('lol')
168 168 assert None == validator.to_python(None)
169 169 pytest.raises(formencode.Invalid, validator.to_python, 'ąćżź')
170 170
171 171
172 172 def test_ValidPasswordsMatch(localizer):
173 173 validator = v.ValidPasswordsMatch(localizer)
174 174 pytest.raises(
175 175 formencode.Invalid,
176 176 validator.to_python, {'password': 'pass',
177 177 'password_confirmation': 'pass2'})
178 178
179 179 pytest.raises(
180 180 formencode.Invalid,
181 181 validator.to_python, {'new_password': 'pass',
182 182 'password_confirmation': 'pass2'})
183 183
184 184 assert {'new_password': 'pass', 'password_confirmation': 'pass'} == \
185 185 validator.to_python({'new_password': 'pass',
186 186 'password_confirmation': 'pass'})
187 187
188 188 assert {'password': 'pass', 'password_confirmation': 'pass'} == \
189 189 validator.to_python({'password': 'pass',
190 190 'password_confirmation': 'pass'})
191 191
192 192
193 193 def test_ValidAuth(localizer, config_stub):
194 194 config_stub.testing_securitypolicy()
195 195 config_stub.include('rhodecode.authentication')
196 config_stub.include('rhodecode.authentication.plugins.auth_rhodecode')
197 config_stub.include('rhodecode.authentication.plugins.auth_token')
196 198
197 199 validator = v.ValidAuth(localizer)
198 200 valid_creds = {
199 201 'username': TEST_USER_REGULAR2_LOGIN,
200 202 'password': TEST_USER_REGULAR2_PASS,
201 203 }
202 204 invalid_creds = {
203 205 'username': 'err',
204 206 'password': 'err',
205 207 }
206 208 assert valid_creds == validator.to_python(valid_creds)
207 209 pytest.raises(
208 210 formencode.Invalid, validator.to_python, invalid_creds)
209 211
210 212
211 213 def test_ValidRepoName(localizer):
212 214 validator = v.ValidRepoName(localizer)
213 215
214 216 pytest.raises(
215 217 formencode.Invalid, validator.to_python, {'repo_name': ''})
216 218
217 219 pytest.raises(
218 220 formencode.Invalid, validator.to_python, {'repo_name': HG_REPO})
219 221
220 222 gr = RepoGroupModel().create(group_name='group_test',
221 223 group_description='desc',
222 224 owner=TEST_USER_ADMIN_LOGIN)
223 225 pytest.raises(
224 226 formencode.Invalid, validator.to_python, {'repo_name': gr.group_name})
225 227
226 228 #TODO: write an error case for that ie. create a repo withinh a group
227 229 # pytest.raises(formencode.Invalid,
228 230 # validator.to_python, {'repo_name': 'some',
229 231 # 'repo_group': gr.group_id})
230 232
231 233
232 234 def test_ValidForkName(localizer):
233 235 # this uses ValidRepoName validator
234 236 assert True
235 237
236 238 @pytest.mark.parametrize("name, expected", [
237 239 ('test', 'test'), ('lolz!', 'lolz'), (' aavv', 'aavv'),
238 240 ('ala ma kota', 'ala-ma-kota'), ('@nooo', 'nooo'),
239 241 ('$!haha lolz !', 'haha-lolz'), ('$$$$$', ''), ('{}OK!', 'OK'),
240 242 ('/]re po', 're-po')])
241 243 def test_SlugifyName(name, expected, localizer):
242 244 validator = v.SlugifyName(localizer)
243 245 assert expected == validator.to_python(name)
244 246
245 247
246 248 def test_ValidForkType(localizer):
247 249 validator = v.ValidForkType(localizer, old_data={'repo_type': 'hg'})
248 250 assert 'hg' == validator.to_python('hg')
249 251 pytest.raises(formencode.Invalid, validator.to_python, 'git')
250 252
251 253
252 254 def test_ValidPath(localizer):
253 255 validator = v.ValidPath(localizer)
254 256 assert TESTS_TMP_PATH == validator.to_python(TESTS_TMP_PATH)
255 257 pytest.raises(
256 258 formencode.Invalid, validator.to_python, '/no_such_dir')
257 259
258 260
259 261 def test_UniqSystemEmail(localizer):
260 262 validator = v.UniqSystemEmail(localizer, old_data={})
261 263
262 264 assert 'mail@python.org' == validator.to_python('MaiL@Python.org')
263 265
264 266 email = TEST_USER_REGULAR2_EMAIL
265 267 pytest.raises(formencode.Invalid, validator.to_python, email)
266 268
267 269
268 270 def test_ValidSystemEmail(localizer):
269 271 validator = v.ValidSystemEmail(localizer)
270 272 email = TEST_USER_REGULAR2_EMAIL
271 273
272 274 assert email == validator.to_python(email)
273 275 pytest.raises(formencode.Invalid, validator.to_python, 'err')
274 276
275 277
276 278 def test_NotReviewedRevisions(localizer):
277 279 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
278 280 validator = v.NotReviewedRevisions(localizer, repo_id)
279 281 rev = '0' * 40
280 282 # add status for a rev, that should throw an error because it is already
281 283 # reviewed
282 284 new_status = ChangesetStatus()
283 285 new_status.author = ChangesetStatusModel()._get_user(TEST_USER_ADMIN_LOGIN)
284 286 new_status.repo = ChangesetStatusModel()._get_repo(HG_REPO)
285 287 new_status.status = ChangesetStatus.STATUS_APPROVED
286 288 new_status.comment = None
287 289 new_status.revision = rev
288 290 Session().add(new_status)
289 291 Session().commit()
290 292 try:
291 293 pytest.raises(formencode.Invalid, validator.to_python, [rev])
292 294 finally:
293 295 Session().delete(new_status)
294 296 Session().commit()
General Comments 0
You need to be logged in to leave comments. Login now