##// END OF EJS Templates
auth: use cache_ttl from a plugin to also cache permissions....
marcink -
r2154:574d07a8 default
parent child Browse files
Show More
@@ -37,7 +37,7 b' from rhodecode.authentication.interface '
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import caches
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import md5_safe, safe_int
40 from rhodecode.lib.utils2 import safe_int
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.model.db import User
43 43 from rhodecode.model.meta import Session
@@ -423,11 +423,14 b' class RhodeCodeAuthPluginBase(object):'
423 423 """
424 424 auth = self.auth(userobj, username, passwd, settings, **kwargs)
425 425 if auth:
426 auth['_plugin'] = self.name
427 auth['_ttl_cache'] = self.get_ttl_cache(settings)
426 428 # check if hash should be migrated ?
427 429 new_hash = auth.get('_hash_migrate')
428 430 if new_hash:
429 431 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
430 432 return self._validate_auth_return(auth)
433
431 434 return auth
432 435
433 436 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
@@ -450,6 +453,19 b' class RhodeCodeAuthPluginBase(object):'
450 453 raise Exception('Missing %s attribute from returned data' % k)
451 454 return ret
452 455
456 def get_ttl_cache(self, settings=None):
457 plugin_settings = settings or self.get_settings()
458 cache_ttl = 0
459
460 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
461 # plugin cache set inside is more important than the settings value
462 cache_ttl = self.AUTH_CACHE_TTL
463 elif plugin_settings.get('cache_ttl'):
464 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
465
466 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
467 return plugin_cache_active, cache_ttl
468
453 469
454 470 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455 471
@@ -578,6 +594,11 b' def get_auth_cache_manager(custom_ttl=No'
578 594 'auth_plugins', 'rhodecode.authentication', custom_ttl)
579 595
580 596
597 def get_perms_cache_manager(custom_ttl=None):
598 return caches.get_cache_manager(
599 'auth_plugins', 'rhodecode.permissions', custom_ttl)
600
601
581 602 def authenticate(username, password, environ=None, auth_type=None,
582 603 skip_missing=False, registry=None, acl_repo_name=None):
583 604 """
@@ -633,25 +654,19 b' def authenticate(username, password, env'
633 654 log.info('Authenticating user `%s` using %s plugin',
634 655 display_user, plugin.get_id())
635 656
636 _cache_ttl = 0
637
638 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
639 # plugin cache set inside is more important than the settings value
640 _cache_ttl = plugin.AUTH_CACHE_TTL
641 elif plugin_settings.get('cache_ttl'):
642 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
643
644 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
657 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
645 658
646 659 # get instance of cache manager configured for a namespace
647 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
660 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
648 661
649 662 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
650 plugin.get_id(), plugin_cache_active, _cache_ttl)
663 plugin.get_id(), plugin_cache_active, cache_ttl)
651 664
652 665 # for environ based password can be empty, but then the validation is
653 666 # on the server that fills in the env data needed for authentication
654 _password_hash = md5_safe(plugin.name + username + (password or ''))
667
668 _password_hash = caches.compute_key_from_params(
669 plugin.name, username, (password or ''))
655 670
656 671 # _authenticate is a wrapper for .auth() method of plugin.
657 672 # it checks if .auth() sends proper data.
@@ -667,11 +682,13 b' def authenticate(username, password, env'
667 682 This function is used internally in Cache of Beaker to calculate
668 683 Results
669 684 """
685 log.debug('auth: calculating password access now...')
670 686 return plugin._authenticate(
671 687 user, username, password, plugin_settings,
672 688 environ=environ or {})
673 689
674 690 if plugin_cache_active:
691 log.debug('Trying to fetch cached auth by %s', _password_hash[:6])
675 692 plugin_user = cache_manager.get(
676 693 _password_hash, createfunc=auth_func)
677 694 else:
@@ -680,7 +697,7 b' def authenticate(username, password, env'
680 697 auth_time = time.time() - start
681 698 log.debug('Authentication for plugin `%s` completed in %.3fs, '
682 699 'expiration time of fetched cache %.1fs.',
683 plugin.get_id(), auth_time, _cache_ttl)
700 plugin.get_id(), auth_time, cache_ttl)
684 701
685 702 log.debug('PLUGIN USER DATA: %s', plugin_user)
686 703
@@ -690,6 +707,8 b' def authenticate(username, password, env'
690 707 # we failed to Auth because .auth() method didn't return proper user
691 708 log.debug("User `%s` failed to authenticate against %s",
692 709 display_user, plugin.get_id())
710
711 # case when we failed to authenticate against all defined plugins
693 712 return None
694 713
695 714
@@ -40,11 +40,10 b' class AuthnPluginSettingsSchemaBase(cola'
40 40 cache_ttl = colander.SchemaNode(
41 41 colander.Int(),
42 42 default=0,
43 description=_('Amount of seconds to cache the authentication response'
44 'call for this plugin. \n'
45 'Useful for long calls like LDAP to improve the '
46 'performance of the authentication system '
47 '(0 means disabled).'),
43 description=_('Amount of seconds to cache the authentication and '
44 'permissions check response call for this plugin. \n'
45 'Useful for expensive calls like LDAP to improve the '
46 'performance of the system (0 means disabled).'),
48 47 missing=0,
49 48 title=_('Auth Cache TTL'),
50 49 validator=colander.Range(min=0, max=None),
@@ -277,10 +277,11 b' class BasicAuth(AuthBasicAuthenticator):'
277 277 _parts = auth.split(':', 1)
278 278 if len(_parts) == 2:
279 279 username, password = _parts
280 if self.authfunc(
280 auth_data = self.authfunc(
281 281 username, password, environ, VCS_TYPE,
282 registry=self.registry, acl_repo_name=self.acl_repo_name):
283 return username
282 registry=self.registry, acl_repo_name=self.acl_repo_name)
283 if auth_data:
284 return {'username': username, 'auth_data': auth_data}
284 285 if username and password:
285 286 # we mark that we actually executed authentication once, at
286 287 # that point we can use the alternative auth code
@@ -24,17 +24,20 b" It's implemented with basic auth functio"
24 24 """
25 25
26 26 import os
27 import re
27 28 import logging
28 29 import importlib
29 import re
30 30 from functools import wraps
31 31
32 import time
32 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 34 from webob.exc import (
34 35 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35 36
36 37 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.authentication.base import (
39 authenticate, get_perms_cache_manager, VCS_TYPE)
40 from rhodecode.lib import caches
38 41 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 42 from rhodecode.lib.base import (
40 43 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
@@ -44,8 +47,7 b' from rhodecode.lib.exceptions import ('
44 47 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
45 48 from rhodecode.lib.middleware import appenlight
46 49 from rhodecode.lib.middleware.utils import scm_app_http
47 from rhodecode.lib.utils import (
48 is_valid_repo, get_rhodecode_base_path, SLUG_RE)
50 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
49 51 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
50 52 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 53 from rhodecode.lib.vcs.backends import base
@@ -242,40 +244,81 b' class SimpleVCS(object):'
242 244 def is_shadow_repo_dir(self):
243 245 return os.path.isdir(self.vcs_repo_name)
244 246
245 def _check_permission(self, action, user, repo_name, ip_addr=None):
247 def _check_permission(self, action, user, repo_name, ip_addr=None,
248 plugin_id='', plugin_cache_active=False, cache_ttl=0):
246 249 """
247 250 Checks permissions using action (push/pull) user and repository
248 name
251 name. If plugin_cache and ttl is set it will use the plugin which
252 authenticated the user to store the cached permissions result for N
253 amount of seconds as in cache_ttl
249 254
250 255 :param action: push or pull action
251 256 :param user: user instance
252 257 :param repo_name: repository name
253 258 """
259
260 # get instance of cache manager configured for a namespace
261 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
262 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
263 plugin_id, plugin_cache_active, cache_ttl)
264
265 # for environ based password can be empty, but then the validation is
266 # on the server that fills in the env data needed for authentication
267 _perm_calc_hash = caches.compute_key_from_params(
268 plugin_id, action, user.user_id, repo_name, ip_addr)
269
270 # _authenticate is a wrapper for .auth() method of plugin.
271 # it checks if .auth() sends proper data.
272 # For RhodeCodeExternalAuthPlugin it also maps users to
273 # Database and maps the attributes returned from .auth()
274 # to RhodeCode database. If this function returns data
275 # then auth is correct.
276 start = time.time()
277 log.debug('Running plugin `%s` permissions check', plugin_id)
278
279 def perm_func():
280 """
281 This function is used internally in Cache of Beaker to calculate
282 Results
283 """
284 log.debug('auth: calculating permission access now...')
254 285 # check IP
255 286 inherit = user.inherit_default_permissions
256 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
257 inherit_from_default=inherit)
287 ip_allowed = AuthUser.check_ip_allowed(
288 user.user_id, ip_addr, inherit_from_default=inherit)
258 289 if ip_allowed:
259 290 log.info('Access for IP:%s allowed', ip_addr)
260 291 else:
261 292 return False
262 293
263 294 if action == 'push':
264 if not HasPermissionAnyMiddleware('repository.write',
265 'repository.admin')(user,
266 repo_name):
295 perms = ('repository.write', 'repository.admin')
296 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
267 297 return False
268 298
269 299 else:
270 300 # any other action need at least read permission
271 if not HasPermissionAnyMiddleware('repository.read',
272 'repository.write',
273 'repository.admin')(user,
274 repo_name):
301 perms = (
302 'repository.read', 'repository.write', 'repository.admin')
303 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
275 304 return False
276 305
277 306 return True
278 307
308 if plugin_cache_active:
309 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
310 perm_result = cache_manager.get(
311 _perm_calc_hash, createfunc=perm_func)
312 else:
313 perm_result = perm_func()
314
315 auth_time = time.time() - start
316 log.debug('Permissions for plugin `%s` completed in %.3fs, '
317 'expiration time of fetched cache %.1fs.',
318 plugin_id, auth_time, cache_ttl)
319
320 return perm_result
321
279 322 def _check_ssl(self, environ, start_response):
280 323 """
281 324 Checks the SSL check flag and returns False if SSL is not present
@@ -376,26 +419,41 b' class SimpleVCS(object):'
376 419 if pre_auth and pre_auth.get('username'):
377 420 username = pre_auth['username']
378 421 log.debug('PRE-AUTH got %s as username', username)
422 if pre_auth:
423 log.debug('PRE-AUTH successful from %s',
424 pre_auth.get('auth_data', {}).get('_plugin'))
379 425
380 426 # If not authenticated by the container, running basic auth
381 427 # before inject the calling repo_name for special scope checks
382 428 self.authenticate.acl_repo_name = self.acl_repo_name
429
430 plugin_cache_active, cache_ttl = False, 0
431 plugin = None
383 432 if not username:
384 433 self.authenticate.realm = self.authenticate.get_rc_realm()
385 434
386 435 try:
387 result = self.authenticate(environ)
436 auth_result = self.authenticate(environ)
388 437 except (UserCreationError, NotAllowedToCreateUserError) as e:
389 438 log.error(e)
390 439 reason = safe_str(e)
391 440 return HTTPNotAcceptable(reason)(environ, start_response)
392 441
393 if isinstance(result, str):
442 if isinstance(auth_result, dict):
394 443 AUTH_TYPE.update(environ, 'basic')
395 REMOTE_USER.update(environ, result)
396 username = result
444 REMOTE_USER.update(environ, auth_result['username'])
445 username = auth_result['username']
446 plugin = auth_result.get('auth_data', {}).get('_plugin')
447 log.info(
448 'MAIN-AUTH successful for user `%s` from %s plugin',
449 username, plugin)
450
451 plugin_cache_active, cache_ttl = auth_result.get(
452 'auth_data', {}).get('_ttl_cache') or (False, 0)
397 453 else:
398 return result.wsgi_application(environ, start_response)
454 return auth_result.wsgi_application(
455 environ, start_response)
456
399 457
400 458 # ==============================================================
401 459 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
@@ -417,12 +475,13 b' class SimpleVCS(object):'
417 475
418 476 # check permissions for this repository
419 477 perm = self._check_permission(
420 action, user, self.acl_repo_name, ip_addr)
478 action, user, self.acl_repo_name, ip_addr,
479 plugin, plugin_cache_active, cache_ttl)
421 480 if not perm:
422 481 return HTTPForbidden()(environ, start_response)
423 482
424 483 # extras are injected into UI object and later available
425 # in hooks executed by rhodecode
484 # in hooks executed by RhodeCode
426 485 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
427 486 extras = vcs_operation_context(
428 487 environ, repo_name=self.acl_repo_name, username=username,
@@ -918,7 +918,7 b' class User(Base, BaseModel):'
918 918 """Update user lastactivity"""
919 919 self.last_activity = datetime.datetime.now()
920 920 Session().add(self)
921 log.debug('updated user %s lastactivity', self.username)
921 log.debug('updated user `%s` last activity', self.username)
922 922
923 923 def update_password(self, new_password):
924 924 from rhodecode.lib.auth import get_crypt_password
@@ -27,8 +27,13 b' from rhodecode.authentication.plugins.au'
27 27 from rhodecode.model import db
28 28
29 29
30 class TestAuthPlugin(RhodeCodeAuthPluginBase):
31
32 def name(self):
33 return 'stub_auth'
34
30 35 def test_authenticate_returns_from_auth(stub_auth_data):
31 plugin = RhodeCodeAuthPluginBase('stub_id')
36 plugin = TestAuthPlugin('stub_id')
32 37 with mock.patch.object(plugin, 'auth') as auth_mock:
33 38 auth_mock.return_value = stub_auth_data
34 39 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
@@ -37,7 +42,7 b' def test_authenticate_returns_from_auth('
37 42
38 43 def test_authenticate_returns_empty_auth_data():
39 44 auth_data = {}
40 plugin = RhodeCodeAuthPluginBase('stub_id')
45 plugin = TestAuthPlugin('stub_id')
41 46 with mock.patch.object(plugin, 'auth') as auth_mock:
42 47 auth_mock.return_value = auth_data
43 48 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
@@ -46,7 +51,7 b' def test_authenticate_returns_empty_auth'
46 51
47 52 def test_authenticate_skips_hash_migration_if_mismatch(stub_auth_data):
48 53 stub_auth_data['_hash_migrate'] = 'new-hash'
49 plugin = RhodeCodeAuthPluginBase('stub_id')
54 plugin = TestAuthPlugin('stub_id')
50 55 with mock.patch.object(plugin, 'auth') as auth_mock:
51 56 auth_mock.return_value = stub_auth_data
52 57 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
@@ -60,7 +65,7 b' def test_authenticate_migrates_to_new_ha'
60 65 new_password = b'new-password'
61 66 new_hash = _RhodeCodeCryptoBCrypt().hash_create(new_password)
62 67 stub_auth_data['_hash_migrate'] = new_hash
63 plugin = RhodeCodeAuthPluginBase('stub_id')
68 plugin = TestAuthPlugin('stub_id')
64 69 with mock.patch.object(plugin, 'auth') as auth_mock:
65 70 auth_mock.return_value = stub_auth_data
66 71 result = plugin._authenticate(
General Comments 0
You need to be logged in to leave comments. Login now