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